<jupyter_start><jupyter_text><jupyter_code>import torch
import numpy as np
torch.__version__
# Similar to numpy
x_torch = torch.ones(5, 4)
print(x_torch)
print(x_torch.shape) #alias for x.size()
print(x_torch.dtype)
print()
y_np = np.ones((5,4))
print(y_np)
print(y_np.shape)
print(y_np.dtype)
# Rand
rand = torch.rand(2,2)
print('rand:', rand)
# From List
from_list = torch.tensor([[1.,2.], [4., 5.]])
print('from_list:', from_list)
x = rand * from_list
print('element wise:', x)
y = rand @ from_list
print('matrix multiplication: ', y)
#  broadcasting also works as expected
arange = torch.arange(2)[:, None]
print('arange:', arange)
res = from_list + arange
print('res:', res)
# in place
res.add_(2)
res<jupyter_output><empty_output><jupyter_text>Any operation that mutates a tensor in-place is post-fixed with an ``_``.    For example: ``x.copy_(y)``, ``x.t_()``, will change ``x``.<jupyter_code>#But some functionalities are diffrent
#You have been warned
x_torch = x_torch.view(2, 2, -1)
print(x_torch)


#Reshape can copy, if layout is not compatible
x_torch = x_torch.reshape(2, 2, -1)
print(x_torch)

y_np = y_np.reshape(2, 2, -1)
print(y_np)

# In torch there is also `reshape` function. But it beahves difrentlly.
# It sometimes can make a copy of an array instead of a reference.<jupyter_output>tensor([[[1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1.]],

        [[1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1.]]])
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. 1.]
  [1. 1. 1. 1. 1.]]

 [[1. 1. 1. 1. 1.]
  [1. 1. 1. 1. 1.]]]<jupyter_text>If you have a one element tensor, use ``.item()`` to get the value as aPython number<jupyter_code>x = torch.randn(1)
print(x)
print(x.item(), type(x.item()))<jupyter_output>tensor([0.2808])
0.28076329827308655 <class 'float'><jupyter_text>Numpy bridgeConverting a Torch Tensor to a NumPy array and vice versa is a breeze.Transformation do not need copying underlying C array. Only `python` properties of the object has to be changed Conclussion: It is fastSometimes it is easier to transofrm data in numpy<jupyter_code>a = torch.ones(5)
print(a)
b = a.numpy()
print(b)<jupyter_output>[1. 1. 1. 1. 1.]<jupyter_text>See how the numpy array changed in value.<jupyter_code>a.add_(1)
print(a)
print(b)<jupyter_output>tensor([2., 2., 2., 2., 2.])
[2. 2. 2. 2. 2.]<jupyter_text>Converting NumPy Array to Torch TensorSee how changing the np array changed the Torch Tensor automatically<jupyter_code>a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
print(b)<jupyter_output>[2. 2. 2. 2. 2.]
tensor([2., 2., 2., 2., 2.], dtype=torch.float64)<jupyter_text>All the Tensors on the CPU except a CharTensor support converting toNumPy and back. CUDA TensorsTensors can be moved onto any device using the ``.to`` method. To interact with each other both tensors have to be on the same device.<jupyter_code># let us run this cell only if CUDA is available
# We will use ``torch.device`` objects to move tensors in and out of GPU
if torch.cuda.is_available():
    device = torch.device("cuda")          # a CUDA device object
    y = torch.ones_like(x, device=device)  # directly create a tensor on GPU
    x = x.to(device)                       # or just use strings ``.to("cuda")``
    z = x + y
    print(z)
    print(z.to("cpu", torch.double))       # ``.to`` can also change dtype together!<jupyter_output>tensor([1.2808], device='cuda:0')
tensor([1.2808], dtype=torch.float64)<jupyter_text>Recommended way for comapatibillityOn systems with multiple GPU's there will be {`cuda:1`, `cuda:2`...} and so on.There is no automatic way to decide which GPU is "free"<jupyter_code>device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
tensor = torch.zeros(2,2).to(device)
tensor
cpu = torch.ones(1)
try:
    cpu + tensor
except Exception as e:
    print(type(e), e)<jupyter_output><class 'RuntimeError'> expected device cpu but got device cuda:0<jupyter_text>Autograd: Automatic Differentiation===================================The ``autograd`` package provides automatic differentiation for all operationson Tensors. It is a define-by-run framework, which means that your backprop isdefined by how your code is run, and that every single iteration can bedifferent.Tensor--------1. If you set its attribute``.requires_grad`` as ``True``, it starts to track all operations on it. 2. Whenyou finish your computation you can call ``.backward()`` and have all thegradients computed automatically. The gradient for this tensor will beaccumulated into ``.grad`` attribute.3. To stop a tensor from tracking history, you can call ``.detach()`` to detachit from the computation history, and to prevent future computation from beingtracked.4. To prevent tracking history (and using memory), you can also wrap the code blockin ``with torch.no_grad():``. This can be particularly helpful when **evaluating amodel** because the model may have trainable parameters with``requires_grad=True``, but for which we don't need the gradients.There’s one more class which is very important for autogradimplementation - a ``Function``.``Tensor`` and ``Function`` are interconnected and build up an acyclicgraph, that encodes a complete history of computation. Each tensor hasa ``.grad_fn`` attribute that references a ``Function`` that has createdthe ``Tensor`` (except for Tensors created by the user - their``grad_fn is None``). Create a tensor and set ``requires_grad=True`` to track computation with it<jupyter_code>x = torch.ones(2, 3, requires_grad=True)
print(x)<jupyter_output>tensor([[1., 1., 1.],
        [1., 1., 1.]], requires_grad=True)<jupyter_text>Do a tensor operation:<jupyter_code>y = x + 2
print(y)
print(y.requires_grad)<jupyter_output>tensor([[3., 3., 3.],
        [3., 3., 3.]], grad_fn=<AddBackward0>)
True<jupyter_text>``y`` was created as a result of an operation, so it has a ``grad_fn``.<jupyter_code>print(y.grad_fn)<jupyter_output><AddBackward0 object at 0x7f22bdd9b278><jupyter_text>Do more operations on ``y``<jupyter_code>z = y * y * 3
out = z.mean()

print(z, out)<jupyter_output>tensor([[27., 27., 27.],
        [27., 27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)<jupyter_text>``.requires_grad_( ... )`` changes an existing Tensor's ``requires_grad``flag in-place. The input flag defaults to ``False`` if not given.<jupyter_code>a = torch.randn(2, 2)
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)<jupyter_output>False
True<jupyter_text>Gradients---------Let's backprop now.Because ``out`` contains a single scalar, ``out.backward()`` isequivalent to ``out.backward(torch.tensor(1.))``.<jupyter_code>out.backward()<jupyter_output><empty_output><jupyter_text>Print gradients d(out)/dx<jupyter_code>print(x.grad)<jupyter_output>tensor([[3., 3., 3.],
        [3., 3., 3.]])<jupyter_text>Now let's take a look at an example of vector-Jacobian product:<jupyter_code>x_1 = torch.ones(2,2, requires_grad=True)
x_2 = (x_1 ** 2 * 3).sum()
x_2.backward(torch.tensor(17.))
print(x_1.grad)
x_3 = (x_1 * 20).sum()
x_3.backward(torch.tensor(2.))
print(x_1.grad)<jupyter_output>tensor([[102., 102.],
        [102., 102.]])
tensor([[142., 142.],
        [142., 142.]])<jupyter_text>You can also stop autograd from tracking history on Tensorswith ``.requires_grad=True`` either by wrapping the code block in``with torch.no_grad():``<jupyter_code>print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
	print((x ** 2).requires_grad)<jupyter_output>True
True
False<jupyter_text>Or by using ``.detach()`` to get a new Tensor with the samecontent but that does not require gradients:<jupyter_code>print(x.requires_grad)
y = x.detach()
print(y.requires_grad)
print(x.eq(y).all())<jupyter_output>True
False
tensor(True)<jupyter_text>Neural NetworksNeural networks can be constructed using the ``torch.nn`` package.Now that you had a glimpse of ``autograd``, ``nn`` depends on``autograd`` to define models and differentiate them.An ``nn.Module`` contains layers, and a method ``forward(input)``\ thatreturns the ``output``.For example, look at this network that classifies digit images:It is a simple feed-forward network. It takes the input, feeds itthrough several layers one after the other, and then finally gives theoutput.A typical training procedure for a neural network is as follows:- Define the neural network that has some learnable parameters (or  weights)- Iterate over a dataset of inputs- Process input through the network- Compute the loss (how far is the output from being correct)- Propagate gradients back into the network’s parameters- Update the weights of the network, typically using a simple update rule:  ``weight = weight - learning_rate * gradient``Define the network------------------Let’s define this network:<jupyter_code>import torch
# torch modules
import torch.nn as nn
# torch functions
import torch.nn.functional as F

class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        # an affine operations: y = Wx + b
        self.fc1 = nn.Linear(32 * 32, 200)
        self.fc2 = nn.Linear(200, 10)
        
    def forward(self, x):
        # x.shape => BATCH_SIZE x Height X Width 
        x = x.view(x.shape[0], -1)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x 
    

net = Net()
print(net)
class Net_with_sequential(nn.Module):

    def __init__(self):
        super(Net_with_sequential, self).__init__()
        # an affine operations: y = Wx + b
        self.dense_layers = nn.Sequential(
            nn.Linear(32 * 32, 200),
            nn.ReLU(),
            nn.Linear(200, 10),
        )
        
    def forward(self, x):
        x = x.view(x.shape[0], -1)
        x = self.dense_layers(x)
        return x 
    

net_ws = Net_with_sequential()
on_GPU=net_ws.to(device)
print(net_ws)<jupyter_output>Net_with_sequential(
  (dense_layers): Sequential(
    (0): Linear(in_features=1024, out_features=200, bias=True)
    (1): ReLU()
    (2): Linear(in_features=200, out_features=10, bias=True)
  )
)<jupyter_text>You just have to define the ``forward`` function, and the ``backward``function (where gradients are computed) is automatically defined for youusing ``autograd``.You can use any of the Tensor operations in the ``forward`` function.The learnable parameters of a model are returned by ``net.parameters()``<jupyter_code>params = list(net.parameters())
print(len(params))
print(params[0].size())  # fc1 .weight<jupyter_output>4
torch.Size([200, 1024])<jupyter_text>Let's try a random 32x32 input.Note: expected input size of this net (LeNet) is 32x32. To use this net onthe MNIST dataset, please resize the images from the dataset to 32x32.<jupyter_code>input = torch.randn(1, 1, 32, 32)
out = net(input) #alias for net.(input)
print(out)<jupyter_output>tensor([[-0.2224,  0.0062,  0.5402,  0.1943,  0.0606, -0.2214,  0.3895, -0.4705,
         -0.0406, -0.0587]], grad_fn=<AddmmBackward>)<jupyter_text>Zero the gradient buffers of all parameters and backprops with randomgradients:<jupyter_code>net.zero_grad()
out.backward(torch.randn(1, 10))<jupyter_output><empty_output><jupyter_text>``torch.nn`` only supports mini-batches. The entire ``torch.nn``    package only supports inputs that are a mini-batch of samples, and not    a single sample.Loss Function-------------A loss function takes the (output, target) pair of inputs, and computes avalue that estimates how far away the output is from the target.<jupyter_code>output = net(input)
target = torch.randn(10)  # a dummy target, for example
target = target.view(1, -1)  # make it the same shape as output
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)<jupyter_output>tensor(0.7363, grad_fn=<MseLossBackward>)<jupyter_text>Now, if you follow ``loss`` in the backward direction, using its``.grad_fn`` attribute, you will see a graph of computations that lookslike this:    input -> view -> linear -> relu -> linear -> relu -> linear          -> MSELoss          -> lossSo, when we call ``loss.backward()``, the whole graph is differentiatedw.r.t. the loss, and all Tensors in the graph that has ``requires_grad=True``will have their ``.grad`` Tensor accumulated with the gradient. Backprop--------To backpropagate the error all we have to do is to ``loss.backward()``.You need to clear the existing gradients though, else gradients will beaccumulated to existing gradients.Now we shall call ``loss.backward()``, and have a look at linear biasgradients before and after the backward.<jupyter_code>net.zero_grad()     # zeroes the gradient buffers of all parameters

print('fc1.bias.grad before backward')
print(net.fc1.bias.grad)

loss.backward()

print('fc1.bias.grad after backward')
print(net.fc1.bias.grad)<jupyter_output>fc1.bias.grad before backward
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.,
        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.,
        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.])
fc1.bias.grad after backward
tensor([ 0.0000, -0.0047, -0.0128,  0.0181,  0.0000, -0.0360,  0.037[...]<jupyter_text>Update the weights------------------The simplest update rule used in practice is the Stochastic GradientDescent (SGD): ``weight = weight - learning_rate * gradient``We can implement this using simple Python code:```learning_rate = 0.01with torch.nograd():    for f in net.parameters():        f.sub_(f.grad.data * learning_rate)```However, as you use neural networks, you want to use various differentupdate rules such as SGD, Nesterov-SGD, Adam, RMSProp, etc.To enable this, we built a small package: ``torch.optim`` thatimplements all these methods. Using it is very simple:<jupyter_code>import torch.optim as optim

# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)

# in your training loop:
optimizer.zero_grad()   # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # Does the update<jupyter_output><empty_output><jupyter_text>Preparing a datasetA lot of effort in solving any machine learning problem goes in to preparing the data. PyTorch provides many tools to make data loading easy and hopefully, to make your code more readable.Dataset class-------------``torch.utils.data.Dataset`` is an abstract class representing adataset.Your custom dataset should inherit ``Dataset`` and override the followingmethods:-  ``__len__`` so that ``len(dataset)`` returns the size of the dataset.-  ``__getitem__`` to support the indexing such that ``dataset[i]`` can   be used to get $i$'th sampleSample of our dataset will be a dict``{'image': image, 'label': label}``. my_SimpleDatasetDataset containes images build form zeroes and ones.Our machine_learning task is to figure out if we have **more ones than zeros** in the picture. (This task is trivial)<jupyter_code>PICTURE_SIZE = 10

class SimpleDataset(torch.utils.data.Dataset):
    def __init__(self, size):
        self.data = torch.zeros(size, PICTURE_SIZE, PICTURE_SIZE)
        self.data[torch.rand_like(self.data) > 0.5] = 1.
        
        self.labels = torch.zeros(size)
        self.labels[self.data.mean(dim=[1,2]) > 0.5] = 1.
    
    def __getitem__(self, item):
        # Accepts scalars, tuples and dictionaries
        return {
                'image': self.data[item],
                'label': self.labels[item]
        }
    def __len__(self):
        return self.data.shape[0]

dataset = SimpleDataset(2**13)
# __len__
print('dataset_len:', len(dataset))

# __getitem__
print(dataset[15])

for x in dataset:
    pass
print('looping sucesfull')<jupyter_output>dataset_len: 8192
{'image': tensor([[1., 1., 0., 1., 1., 0., 1., 0., 1., 0.],
        [1., 0., 1., 0., 0., 1., 1., 1., 0., 0.],
        [0., 0., 1., 1., 0., 0., 1., 1., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 0., 0., 0.],
        [1., 0., 0., 0., 0., 1., 0., 1., 0., 1.],
        [1., 0., 0., 1., 0., 0., 1., 0., 1., 0.],
        [1., 1., 0., 0., 0., 1., 1., 0., 1., 0.],
        [1., 1., 1., 1., 1., 1., 0., 1., 0., 1.],
        [1., 1., 0., 1., 0., 0., 1., 0., 1., 0.],
        [0., 1., 0., 1., 0., 0., 1., 0., 0., 0.]]), 'label': tensor(0.)}
looping sucesfull<jupyter_text>However, we are losing a lot of features by using a simple ``for`` loop toiterate over the data. In particular, we are missing out on:-  Batching the data-  Shuffling the data-  Load the data in parallel using ``multiprocessing`` workers.``torch.utils.data.DataLoader`` is an iterator which provides all thesefeatures. Parameters used below should be clear.<jupyter_code>BATCH_SIZE = 32

dataloader = torch.utils.data.DataLoader(
        dataset,
        batch_size=BATCH_SIZE,
        shuffle=True,
        num_workers=2
)<jupyter_output><empty_output><jupyter_text>NeuralNet definition<jupyter_code>HIDDEN_SIZE = 800

class CustomDenseNetwork(nn.Module):
    def __init__(self):
        super(CustomDenseNetwork, self).__init__()
        self.hidden = nn.Sequential(
                nn.Linear(PICTURE_SIZE * PICTURE_SIZE, HIDDEN_SIZE),
                nn.ReLU(),
                nn.Linear(HIDDEN_SIZE, 1)
        )
    def forward(self, x):
        x = x.view(-1, PICTURE_SIZE * PICTURE_SIZE)
        x = self.hidden(x)
        return x
EPOCHS = 30

# Move model weigths to device
device = ('cuda:0' if torch.cuda.is_available() else 'cpu')
net = CustomDenseNetwork().to(device)

optimizer = torch.optim.SGD(net.parameters(), lr=0.005)

for e in range(EPOCHS):
    loss_avg = 0.
    acc_avg = 0.
    for i, batch_data in enumerate(dataloader):
        # Start with zeroing .grad fields in model
        net.zero_grad()
        
        # Move data to device
        images = batch_data['image'].to(device)
        labels = batch_data['label'].to(device)
        
        #Run neural net
        out = net(images)

        #Compute loss and apply autograd for model parameters
        loss = ((out.view(-1) - labels)** 2).mean()
        loss.backward()
        
        #Update network weigths
        optimizer.step()

        # Statistics generation
        res = torch.where(
                out.view(-1) > 0.5,
                torch.tensor(1., device=device),
                torch.tensor(0., device=device)
        )
        acc = (res == labels).sum()
        loss_avg += loss
        acc_avg += acc
        if i % 30 == 0 and i != 0:
            print(f'epoch:{e + 1:2d} step: {(i):3d} loss: {loss_avg / 30:.3f} acc: {acc_avg / (BATCH_SIZE * 30):.3f}')
            acc_avg = 0.
            loss_avg = 0.
        # End of statistics generation<jupyter_output>epoch: 1 step:  30 loss: 0.243 acc: 0.631
epoch: 1 step:  60 loss: 0.227 acc: 0.631
epoch: 1 step:  90 loss: 0.229 acc: 0.620
epoch: 1 step: 120 loss: 0.218 acc: 0.667
epoch: 1 step: 150 loss: 0.212 acc: 0.701
epoch: 1 step: 180 loss: 0.206 acc: 0.711
epoch: 1 step: 210 loss: 0.203 acc: 0.720
epoch: 1 step: 240 loss: 0.202 acc: 0.730
epoch: 2 step:  30 loss: 0.199 acc: 0.786
epoch: 2 step:  60 loss: 0.186 acc: 0.807
epoch: 2 step:  90 loss: 0.184 acc: 0.784
epoch: 2 step: 120 loss: 0.182 acc: 0.809
epoch: 2 step: 150 loss: 0.180 acc: 0.809
epoch: 2 step: 180 loss: 0.178 acc: 0.802
epoch: 2 step: 210 loss: 0.176 acc: 0.804
epoch: 2 step: 240 loss: 0.176 acc: 0.796
epoch: 3 step:  30 loss: 0.169 acc: 0.883
epoch: 3 step:  60 loss: 0.162 acc: 0.831
epoch: 3 step:  90 loss: 0.163 acc: 0.831
epoch: 3 step: 120 loss: 0.158 acc: 0.852
epoch: 3 step: 150 loss: 0.162 acc: 0.828
epoch: 3 step: 180 loss: 0.162 acc: 0.843
epoch: 3 step: 210 loss: 0.154 acc: 0.864
epoch: 3 step: 240 loss: 0.151 acc[...]<jupyter_text>ProTip: mnist InMemDataLoder**Fast DataLoader** for **small datasets** (MNIST for example). On Colab, this code snippet sagnificantlly improves data loading time, which can have huge impact on duration of the traning.<jupyter_code>class InMemDataLoader(object):
    __initialized = False
    def __init__(self, tensors, batch_size=1, shuffle=False, sampler=None,
                 batch_sampler=None, drop_last=False):
        """A torch dataloader that fetches data from memory."""
        tensors = [torch.tensor(tensor) for tensor in tensors]
        dataset = torch.utils.data.TensorDataset(*tensors)
        self.dataset = dataset
        self.batch_size = batch_size
        self.drop_last = drop_last
        
        if batch_sampler is not None:
            if batch_size > 1 or shuffle or sampler is not None or drop_last:
                raise ValueError('batch_sampler option is mutually exclusive '
                                 'with batch_size, shuffle, sampler, and '
                                 'drop_last')
            self.batch_size = None
            self.drop_last = None

        if sampler is not None and shuffle:
            raise ValueError('sampler option is mutually exclusive with '
                             'shuffle')
            
        if batch_sampler is None:
            if sampler is None:
                if shuffle:
                    sampler = torch.utils.data.RandomSampler(dataset)
                else:
                    sampler = torch.utils.data.SequentialSampler(dataset)
            batch_sampler = torch.utils.data.BatchSampler(sampler, batch_size, drop_last)

        self.sampler = sampler
        self.batch_sampler = batch_sampler
        self.__initialized = True
    
    def __setattr__(self, attr, val):
        if self.__initialized and attr in ('batch_size', 'sampler', 'drop_last'):
            raise ValueError('{} attribute should not be set after {} is '
                             'initialized'.format(attr, self.__class__.__name__))

        super(InMemDataLoader, self).__setattr__(attr, val)

    def __iter__(self):
        for batch_indices in self.batch_sampler:
            yield self.dataset[batch_indices]

    def __len__(self):
        return len(self.batch_sampler)
    
    def to(self, device):
        self.dataset.tensors = tuple(t.to(device) for t in self.dataset.tensors)
        return self
##### HOW TO USE! ###
# Download raw data
# !command runs shell command in jupyter notebook
!pip install -q gdown httpimport
![ -e mnist.npz ] || gdown 'https://drive.google.com/uc?id=1QPaC3IKB_5tX6yIZgRgkpcqFrfVqPTXU' -O mnist.npz

#Extracting data
with np.load('mnist.npz') as data:
    mnist_full_train_data = data['train_data'].astype('float32') / 255.0


#Fast loader initialziation
train_loader = InMemDataLoader(
    (mnist_full_train_data, ), batch_size=25, shuffle=True)
next(iter(train_loader))<jupyter_output>Downloading...
From: https://drive.google.com/uc?id=1QPaC3IKB_5tX6yIZgRgkpcqFrfVqPTXU
To: /content/mnist.npz
55.4MB [00:00, 152MB/s]
{'fraction_non_alphanumeric': 0.1480499486828601, 'fraction_numerical': 0.047083475880944234, 'mean_word_length': 3.214272841953505, 'pattern_counts': {'":': 0, '<': 101, '<?xml version=': 0, '>': 114, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 74, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '29', 'dir': 'jupyter-structured-clean-dedup', 'id': '97847', 'license': 'MIT', 'n_tokens_mistral': 8797, 'n_tokens_neox': 7894, 'n_words': 2585, 'path': 'NN-DL/Lectures/02_pytorch_introduction.ipynb', 'repo_name': 'TheFebrin/DataScience', 'size': '23384'}