IntroPyTorch
Introduction to PyTorch
This notebook is a part of AI for Beginners Curricula. Visit the repository for complete set of learning materials.
Neural Frameworks
We have learnt that to train neural networks you need:
- Quickly multiply matrices (tensors)
- Compute gradients to perform gradient descent optimization
What neural network frameworks allow you to do:
- Operate with tensors on whatever compute is available, CPU or GPU, or even TPU
- Automatically compute gradients (they are explicitly programmed for all built-in tensor functions)
Optionally:
- Neural Network constructor / higher level API (describe network as a sequence of layers)
- Simple training functions (
fit, as in Scikit Learn) - A number of optimization algorithms in addition to gradient descent
- Data handling abstractions (that will ideally work on GPU, too)
Most Popular Frameworks
- Tensorflow 1.x - first widely available framework (Google). Allowed to define static computation graph, push it to GPU, and explicitly evaluate it
- PyTorch - a framework from Facebook that is growing in popularity
- Keras - higher level API on top of Tensorflow/PyTorch to unify and simplify using neural networks (Francois Chollet)
- Tensorflow 2.x + Keras - new version of Tensorflow with integrated Keras functionality, which supports dynamic computation graph, allowing to perform tensor operations very similar to numpy (and PyTorch)
In this Notebook, we will learn to use PyTorch. You need to make sure that you have recent version of PyTorch installed - to do it, follow the instructions on their site. It is normally as simple as doing
pip install torch torchvision
or
conda install pytorch -c pytorch
'1.11.0+cu113'
Basic Concepts: Tensor
Tensor is a multi-dimensional array. It is very convenient to use tensors to represent different types of data:
- 400x400 - black-and-white picture
- 400x400x3 - color picture
- 16x400x400x3 - minibatch of 16 color pictures
- 25x400x400x3 - one second of 25-fps video
- 8x25x400x400x3 - minibatch of 8 1-second videos
Simple Tensors
You can easily create simple tensors from lists of np-arrays, or generate random ones:
tensor([[1, 2],
[3, 4]])
tensor([[ 0.8995, -1.6137, 1.4489],
[-0.2796, -2.1443, -2.4618],
[-0.2358, -0.4249, -0.0716],
[-0.1267, -0.6382, 0.0593],
[-0.4956, 1.7054, 0.3874],
[ 1.3479, -1.6329, 0.2793],
[ 1.1211, -1.5430, 0.7186],
[-1.5197, 0.5559, -1.6421],
[ 0.1900, -0.4175, -0.3922],
[ 1.8994, 0.1497, -0.7039]])
You can use arithmetic operations on tensors, which are performed element-wise, as in numpy. Tensors are automatically expanded to required dimension, if needed. To extract numpy-array from tensor, use .numpy():
tensor([[ 0.0000, 0.0000, 0.0000],
[-2.0583, -0.5631, 1.4932],
[-1.0613, -1.0738, 2.2078],
[-1.5101, 0.5896, 2.4722],
[-2.8219, -2.0846, 1.2405],
[ 0.8706, -0.2485, 2.3679],
[-1.6590, 0.1935, 1.8698],
[-0.3316, 0.8065, 1.6490],
[-1.5788, -1.1844, -0.4816],
[ 0.0680, -1.4526, 1.8159]])
[3.887189 2.1276016 0.17371987]
In-place and out-of-place Operations
Tensor operations such as +/add return new tensors. However, sometimes you need to modify the existing tensor in-place. Most of the operations have their in-place counterparts, which end with _:
Result when adding out-of-place: tensor(8) Result after adding in-place: tensor(8)
This is how we can compute the sum or all rows in a matrix in a naive way:
tensor([ 3.4945, 2.5325, -2.8684])
But it is much better to use
tensor([ 3.4945, 2.5325, -2.8684])
You can read more on PyTorch tensors in the official documentation
Computing Gradients
For back propagation, you need to compute gradients. We can set any PyTorch Tensor's attribute requires_grad to True, which will result in all operations with this tensor being tracked for gradient calculations. To compute the gradients, you need to call backward() method, after which the gradient will become available using grad attribute:
tensor([[-0.1728, 0.0913],
[-0.1666, -0.1942]])
To be more precise, PyTorch automatically accumulates gradients. If you specify retain_graph=True when calling backward, computational graph will be preserved, and new gradient is added to the grad field. In order to restart computing gradients from scratch, we need to reset grad field to 0 explicitly by calling zero_():
tensor([[-0.5185, 0.2739],
[-0.4998, -0.5826]])
tensor([[-0.1728, 0.0913],
[-0.1666, -0.1942]])
To compute gradients, PyTorch creates and maintains compute graph. For each tensor that has the requires_grad flag set to True, PyTorch maintains a special function called grad_fn, which computes the derivative of the expression according to chain differentiation rule:
tensor(0.9143, grad_fn=<MeanBackward0>)
Here c is computed using mean function, thus grad_fn point to a function called MeanBackward.
In most of the cases, we want PyTorch to compute gradient of a scalar function (such as loss function). However, if we want to compute the gradient of a tensor with respect to another tensor, PyTorch allows us to compute the product of a Jacobian matrix and a given vector.
Suppose we have a vector function , where and , then a gradient of with respect to is defined by a Jacobian:
Instead of giving us access to the whole Jacobian, PyTorch computes the product of Jacobian with some vector
. In order to do that, we need to call backward and pass v as an argument. The size of v should be the same as the size of the original tensor, with respect to which we compute the gradient.
tensor([[-0.8642, 0.0913],
[-0.1666, -0.9710]])
More on computing Jacobians in PyTorch can be found in official documentation
Example 0: Optimization Using Gradient Descent
Let's try to use automatic differentiation to find a minimum of a simple two-variable function . Let tensor x hold the current coordinates of a point. We start with some starting point , and compute the next point in a sequence using gradient descent formula:
Here is so-called learning rage (we will denote it by lr in the code), and - gradient of .
To begin, let's define starting value of x and the function f:
Now let's do 15 iterations of gradient descent. In each iteration, we will update x coordinates and print them, to make sure that we are approaching the minimum point at (3,-2):
Step 0: x[0]=0.6000000238418579, x[1]=-0.4000000059604645 Step 1: x[0]=1.0800000429153442, x[1]=-0.7200000286102295 Step 2: x[0]=1.4639999866485596, x[1]=-0.9760000705718994 Step 3: x[0]=1.7711999416351318, x[1]=-1.1808000802993774 Step 4: x[0]=2.0169599056243896, x[1]=-1.3446400165557861 Step 5: x[0]=2.2135679721832275, x[1]=-1.4757120609283447 Step 6: x[0]=2.370854377746582, x[1]=-1.5805696249008179 Step 7: x[0]=2.4966835975646973, x[1]=-1.6644556522369385 Step 8: x[0]=2.597346782684326, x[1]=-1.7315645217895508 Step 9: x[0]=2.677877426147461, x[1]=-1.7852516174316406 Step 10: x[0]=2.7423019409179688, x[1]=-1.8282012939453125 Step 11: x[0]=2.793841600418091, x[1]=-1.8625609874725342 Step 12: x[0]=2.835073232650757, x[1]=-1.8900487422943115 Step 13: x[0]=2.868058681488037, x[1]=-1.912039041519165 Step 14: x[0]=2.894446849822998, x[1]=-1.929631233215332
Example 1: Linear Regression
Now we know enough to solve the classical problem of Linear regression. Let's generate small synthetic dataset:
<matplotlib.collections.PathCollection at 0x20b8e1f1ca0>
Linear regression is defined by a straight line , where are model parameters that we need to find. An error on our dataset (also called loss function) can be defined as mean square error:
Let's define our model and loss function:
We will train the model on a series of minibatches. We will use gradient descent, adjusting model parameters using the following formulae:
Let's do the training. We will do several passes through the dataset (so-called epochs), divide it into minibatches and call the function defined above:
Epoch 0: last batch loss = 94.5247 Epoch 1: last batch loss = 9.3428 Epoch 2: last batch loss = 1.4166 Epoch 3: last batch loss = 0.5224 Epoch 4: last batch loss = 0.3807 Epoch 5: last batch loss = 0.3495 Epoch 6: last batch loss = 0.3413 Epoch 7: last batch loss = 0.3390 Epoch 8: last batch loss = 0.3384 Epoch 9: last batch loss = 0.3382
We now have obtained optimized parameters and . Note that their values are similar to the original values used when generating the dataset ()
(tensor([1.8617], requires_grad=True), tensor([1.0711], requires_grad=True))
[<matplotlib.lines.Line2D at 0x20b8e30a850>]
Computations on GPU
To use GPU for computations, PyTorch supports moving tensors to GPU and building computational graph for GPU. Traditionally, in the beginning of our code we define available computation device device (which is either cpu or cuda), and then move all tensors to this device using a call .to(device). We can also create tensors on the specified device upfront, by passing the parameter device=... to tensor creation code. Such code works without changes both on CPU and GPU:
Doing computations on cpu Epoch 0: last batch loss = 94.5247 Epoch 1: last batch loss = 9.3428 Epoch 2: last batch loss = 1.4166 Epoch 3: last batch loss = 0.5224 Epoch 4: last batch loss = 0.3807 Epoch 5: last batch loss = 0.3495 Epoch 6: last batch loss = 0.3413 Epoch 7: last batch loss = 0.3390 Epoch 8: last batch loss = 0.3384 Epoch 9: last batch loss = 0.3382
Example 2: Classification
Now we will consider binary classification problem. A good example of such a problem would be a tumour classification between malignant and benign based on it's size and age.
The core model is similar to regression, but we need to use different loss function. Let's start by generating sample data:
C:\Users\dmitryso\AppData\Local\Temp/ipykernel_89704/2721537645.py:17: UserWarning: Matplotlib is currently using module://matplotlib_inline.backend_inline, which is a non-GUI backend, so cannot show the figure. fig.show()
Training One-Layer Perceptron
Let's use PyTorch gradient computing machinery to train one-layer perceptron.
Our neural network will have 2 inputs and 1 output. The weight matrix will have size , and bias vector -- .
To make our code more structured, let's group all parameters into a single class:
Note that we use
W.data.zero_()instead ofW.zero_(). We need to do this, because we cannot directly modify a tensor that is being tracked using Autograd mechanism.
Core model will be the same as in previous example, but loss function will be a logistic loss. To apply logistic loss, we need to get the value of probability as the output of our network, i.e. we need to bring the output to the range [0,1] using sigmoid activation function: .
If we get the probability for the i-th input value corresponding to the actual class , we compute the loss as .
In PyTorch, both those steps (applying sigmoid and then logistic loss) can be done using one call to binary_cross_entropy_with_logits function. Since we are training our network in minibatches, we need to average out the loss across all elements of a minibatch - and that is also done automatically by binary_cross_entropy_with_logits function:
The call to
binary_crossentropy_with_logitsis equivalent to a call tosigmoid, followed by a call tobinary_crossentropy
To loop through our data, we will use built-in PyTorch mechanism for managing datasets. It is based on two concepts:
- Dataset is the main source of data, it can be either Iterable or Map-style
- Dataloader is responsible for loading the data from a dataset and splitting it into minibatches.
In our case, we will define a dataset based on a tensor, and split it into minibatches of 16 elements. Each minibatch contains two tensors, input data (size=16x2) and labels (a vector of length 16 of integer type - class number).
[tensor([[ 1.5442, 2.5290], , [-1.6284, 0.0772], , [-1.7141, 2.4770], , [-1.4951, 0.7320], , [-1.6899, 0.9243], , [-0.9474, -0.7681], , [ 3.8597, -2.2951], , [-1.3944, 1.4300], , [ 4.3627, 3.1333], , [-1.0973, -1.7011], , [-2.5532, -0.0777], , [-1.2661, -0.3167], , [ 0.3921, 1.8406], , [ 2.2091, -1.6045], , [ 1.8383, -1.4861], , [ 0.7173, -0.9718]]), , tensor([1., 0., 0., 0., 0., 0., 1., 0., 1., 0., 0., 0., 1., 1., 1., 1.])]
Now we can loop through the whole dataset to train our network for 15 epochs:
Epoch 0: last batch loss = 0.6491 Epoch 1: last batch loss = 0.6064 Epoch 2: last batch loss = 0.5822 Epoch 3: last batch loss = 0.5679 Epoch 4: last batch loss = 0.5592 Epoch 5: last batch loss = 0.5537 Epoch 6: last batch loss = 0.5501 Epoch 7: last batch loss = 0.5478 Epoch 8: last batch loss = 0.5463 Epoch 9: last batch loss = 0.5454 Epoch 10: last batch loss = 0.5447 Epoch 11: last batch loss = 0.5443 Epoch 12: last batch loss = 0.5441 Epoch 13: last batch loss = 0.5439 Epoch 14: last batch loss = 0.5438
Obtained parameters:
tensor([[ 0.1330],
[-0.2810]], requires_grad=True) tensor([0.], requires_grad=True)
To make sure our training worked, let's plot the line that separates two classes. Separation line is defined by the equation
C:\Users\dmitryso\AppData\Local\Temp/ipykernel_89704/2721537645.py:17: UserWarning: Matplotlib is currently using module://matplotlib_inline.backend_inline, which is a non-GUI backend, so cannot show the figure. fig.show()
Not let's compute the accuracy on the validation dataset:
tensor(0.7333)
Let's explain what is going on here:
predis the vector of predicted probabilities for the whole validation dataset. We compute it by running original validation datavalid_xthrough our network, and applyingsigmoidto get probabilities.pred.view(-1)creates a flattened view of the original tensor.viewis similar toreshapefunction in numpy.pred.view(-1)>0.5returns a boolean tensor or truth value showing the predicted class (False = class 0, True = class 1)- Similarly,
torch.tensor(valid_labels)>0.5)creates the boolean tensor of truth values for validation labels - We compare those two tensors element-wise, and get another boolean tensor, where
Truecorresponds to correct prediction, andFalse- to incorrect. - We convert that tensor to floating point, and take it's mean value using
torch.mean- that is the desired accuracy
Neural Networks and Optimizers
In PyTorch, a special module torch.nn.Module is defined to represent a neural network. There are two methods to define your own neural network:
- Sequential, where you just specify a list of layers that comprise your network
- As a class inherited from
torch.nn.Module
First method allows you to specify standard networks with sequential composition of layers, while the second one is more flexible, and gives an opportunity to express networks of arbitrary complex architectures.
Inside modules, you can use standard layers, such as:
Linear- dense linear layer, equivalent to one-layered perceptron. It has the same architecture as we have defined above for our networkSoftmax,Sigmoid,ReLU- layers that correspond to activation functions- There are also other layers for special network types - convolution, recurrent, etc. We will revisit many of them later in the course.
Most of the activation function and loss functions in PyTorch are available in two form: as a function (inside
torch.nn.functionalnamespace) and as a layer (insidetorch.nnnamespace). For activation functions, it is often easier to use functional elements fromtorch.nn.functional, without creating separate layer object.
If we want to train one-layer perceptron, we can just use one built-in Linear layer:
[Parameter containing: tensor([[-0.0422, 0.1821]], requires_grad=True), Parameter containing: tensor([0.6582], requires_grad=True)]
As you can see, parameters() method returns all the parameters that need to be adjusted during training. They correspond to weight matrix and bias . You may note that they have requires_grad set to True, because we need to compute gradients with respect to parameters.
PyTorch also contains built-in optimizers, which implement optimization methods such as gradient descent. Here is how we can define a stochastic gradient descent optimizer:
Using the optimizer, our training loop will look like this:
Epoch 0: last batch loss = 0.7596041560173035, val acc = 0.5333333611488342 Epoch 1: last batch loss = 0.6602361798286438, val acc = 0.6000000238418579 Epoch 2: last batch loss = 0.5847358107566833, val acc = 0.6666666865348816 Epoch 3: last batch loss = 0.5263020992279053, val acc = 0.7333333492279053 Epoch 4: last batch loss = 0.48015740513801575, val acc = 0.800000011920929 Epoch 5: last batch loss = 0.4430023431777954, val acc = 0.8666666746139526 Epoch 6: last batch loss = 0.41254672408103943, val acc = 0.8666666746139526 Epoch 7: last batch loss = 0.3871781527996063, val acc = 0.800000011920929 Epoch 8: last batch loss = 0.3657420873641968, val acc = 0.800000011920929 Epoch 9: last batch loss = 0.34739670157432556, val acc = 0.800000011920929
You may notice that to apply our network to input data we can use
net(x)instead ofnet.forward(x), becausenn.Moduleimplements Python__call__()function
Taking this into account, we can define generic train function:
Epoch 0: last batch loss = 0.48486900329589844, val acc = 0.7333333492279053 Epoch 1: last batch loss = 0.41338109970092773, val acc = 0.800000011920929 Epoch 2: last batch loss = 0.35756850242614746, val acc = 0.800000011920929 Epoch 3: last batch loss = 0.31495171785354614, val acc = 0.800000011920929 Epoch 4: last batch loss = 0.2824164032936096, val acc = 0.800000011920929 Epoch 5: last batch loss = 0.2572754919528961, val acc = 0.800000011920929 Epoch 6: last batch loss = 0.23751722276210785, val acc = 0.800000011920929 Epoch 7: last batch loss = 0.2217157930135727, val acc = 0.800000011920929 Epoch 8: last batch loss = 0.2088666558265686, val acc = 0.800000011920929 Epoch 9: last batch loss = 0.19824868440628052, val acc = 0.800000011920929
Defining Network as a Sequence of Layers
Now let's train multi-layered perceptron. It can be defined just by specifying a sequence of layers. The resulting object will automatically inherit from Module, e.g. it will also have parameters method that will return all parameters of the whole network.
Sequential( (0): Linear(in_features=2, out_features=5, bias=True) (1): Sigmoid() (2): Linear(in_features=5, out_features=1, bias=True) )
We can train this multi-layered network using the function train that we have defined above:
Epoch 0: last batch loss = 0.5835739970207214, val acc = 0.800000011920929 Epoch 1: last batch loss = 0.4642275869846344, val acc = 0.800000011920929 Epoch 2: last batch loss = 0.35158076882362366, val acc = 0.800000011920929 Epoch 3: last batch loss = 0.26132312417030334, val acc = 0.800000011920929 Epoch 4: last batch loss = 0.19465585052967072, val acc = 0.800000011920929 Epoch 5: last batch loss = 0.14735405147075653, val acc = 0.800000011920929 Epoch 6: last batch loss = 0.11454981565475464, val acc = 0.800000011920929 Epoch 7: last batch loss = 0.09244414418935776, val acc = 0.800000011920929 Epoch 8: last batch loss = 0.07805468142032623, val acc = 0.800000011920929 Epoch 9: last batch loss = 0.06894762068986893, val acc = 0.800000011920929
Defining a Network as a Class
Using a class inherited from torch.nn.Module is a more flexible method, because we can define any computations inside it. Module automates a lot of things, eg. it automatically understands all internal variables that are PyTorch layers, and gathers their parameters for optimization. You just need to define all layers of the network as members of the class:
MyNet( (fc1): Linear(in_features=2, out_features=10, bias=True) (func): ReLU() (fc2): Linear(in_features=10, out_features=1, bias=True) )
Epoch 0: last batch loss = 0.7821246981620789, val acc = 0.46666666865348816 Epoch 1: last batch loss = 0.7457502484321594, val acc = 0.5333333611488342 Epoch 2: last batch loss = 0.7120334506034851, val acc = 0.5333333611488342 Epoch 3: last batch loss = 0.6811249256134033, val acc = 0.6666666865348816 Epoch 4: last batch loss = 0.6533011794090271, val acc = 0.7333333492279053 Epoch 5: last batch loss = 0.627849280834198, val acc = 0.7333333492279053 Epoch 6: last batch loss = 0.6030643582344055, val acc = 0.800000011920929 Epoch 7: last batch loss = 0.5775002837181091, val acc = 0.800000011920929 Epoch 8: last batch loss = 0.5522137880325317, val acc = 0.8666666746139526 Epoch 9: last batch loss = 0.5250465869903564, val acc = 0.8666666746139526
Task 1: Plot the graphs of loss function and accuracy on training and validation data during training
Task 2: Try to solve MNIST classificiation problem using this code. Hint: use crossentropy_with_logits as a loss function.
Defining a Network as PyTorch Lightning Module
Let's wrap the written PyTorch model code in PyTorch Lightining module. This allows to work with your model more conveniently and flexibly using various Lightining methods for training and accuracy testing.
First we need to install and import PyTorch Lightining. This can be done with the command
pip install pytorch-lightning
or
conda install -c conda-forge pytorch-lightning
In order for our code to work in Lightning, we need to do the following:
- Create a subclass of
pl.LightningModuleand add to it model architecture in__init__method andforwardpass method. - Move used optimizer to the
configure_optimizers()method. - Define the training and validation process in methods
training_stepandvalidation_steprespectively. - (Optional) Implement a testing (
test_stepmethod) and prediction process (predict_stepmethod).
It should also be understood that PyTorch Lightning has a built-in translation of models to different devices, depending on where the incoming data from the DataLoaders is located. Therefore, all calls .cuda() or .to(device) should be removed from the code.
Let's also add validation Dataset and DataLoader:
Now our model is ready for training. In Pytorch Lightning, this process is implemented through an object of the Trainer class, which essentially "mixes" the model with any datasets.
GPU available: True, used: True TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] | Name | Type | Params -------------------------------- 0 | fc1 | Linear | 30 1 | func | ReLU | 0 2 | fc2 | Linear | 11 -------------------------------- 41 Trainable params 0 Non-trainable params 41 Total params 0.000 Total estimated model params size (MB)
Sanity Checking: 0it [00:00, ?it/s]
Epoch 0: val loss = 0.7213451266288757 val acc = 0.3333333432674408
Training: 0it [00:00, ?it/s]
Validation: 0it [00:00, ?it/s]
Epoch 1: val loss = 0.7164624333381653 val acc = 0.3333333432674408
Validation: 0it [00:00, ?it/s]
Epoch 2: val loss = 0.7117107510566711 val acc = 0.3333333432674408
Validation: 0it [00:00, ?it/s]
Epoch 3: val loss = 0.7070826292037964 val acc = 0.3333333432674408
Validation: 0it [00:00, ?it/s]
Epoch 4: val loss = 0.7025845050811768 val acc = 0.3333333432674408
Validation: 0it [00:00, ?it/s]
Epoch 5: val loss = 0.6982313990592957 val acc = 0.2666666805744171
Validation: 0it [00:00, ?it/s]
Epoch 6: val loss = 0.6939730644226074 val acc = 0.2666666805744171
Validation: 0it [00:00, ?it/s]
Epoch 7: val loss = 0.6897947192192078 val acc = 0.2666666805744171
Validation: 0it [00:00, ?it/s]
Epoch 8: val loss = 0.6857149004936218 val acc = 0.2666666805744171
Validation: 0it [00:00, ?it/s]
Epoch 9: val loss = 0.6817294359207153 val acc = 0.46666669845581055
Validation: 0it [00:00, ?it/s]
Epoch 10: val loss = 0.6778346300125122 val acc = 0.46666669845581055
Validation: 0it [00:00, ?it/s]
Epoch 11: val loss = 0.6740307807922363 val acc = 0.46666669845581055
Validation: 0it [00:00, ?it/s]
Epoch 12: val loss = 0.6703100204467773 val acc = 0.5333333611488342
Validation: 0it [00:00, ?it/s]
Epoch 13: val loss = 0.6666661500930786 val acc = 0.5333333611488342
Validation: 0it [00:00, ?it/s]
Epoch 14: val loss = 0.663104772567749 val acc = 0.6000000238418579
Validation: 0it [00:00, ?it/s]
Epoch 15: val loss = 0.6596227884292603 val acc = 0.6000000238418579
Validation: 0it [00:00, ?it/s]
Epoch 16: val loss = 0.6562108993530273 val acc = 0.6000000238418579
Validation: 0it [00:00, ?it/s]
Epoch 17: val loss = 0.652866542339325 val acc = 0.6666666865348816
Validation: 0it [00:00, ?it/s]
Epoch 18: val loss = 0.649587094783783 val acc = 0.6666666865348816
Validation: 0it [00:00, ?it/s]
Epoch 19: val loss = 0.6463660597801208 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 20: val loss = 0.6432054042816162 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 21: val loss = 0.6401029229164124 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 22: val loss = 0.6370567679405212 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 23: val loss = 0.6340654492378235 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 24: val loss = 0.6311267018318176 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 25: val loss = 0.6282366514205933 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 26: val loss = 0.6253498792648315 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 27: val loss = 0.6225143671035767 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 28: val loss = 0.6197248101234436 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 29: val loss = 0.6169812679290771 val acc = 0.7333333492279053
Validation: 0it [00:00, ?it/s]
Epoch 30: val loss = 0.6142613887786865 val acc = 0.8000000715255737
Takeaways
- PyTorch allows you to operate on tensors at low level, you have most flexibility.
- There are convenient tools to work with data, such as Datasets and Dataloaders.
- You can define neural network architectures using
Sequentialsyntax, or inheriting a class fromtorch.nn.Module - For even simpler approach to defining and training a network - look into PyTorch Lightning