install.packages(torch)
::install_torch() ## si vous avez un GPU compatible avec CUDA
torch## torch::install_torch(type = "cpu") ## sinon
ResNet : comparaison {torch}
et pytorch
Intro
Le but est de comparer la syntaxe utilisée par les deux langages, seuls les exemples en R
sont exécutés (difficultés de faire un quarto avec deux langages, voir issue ici ou de faire tourner pytorch
avec {reticulate}
). L’exemple d’application est un ResNet.
Pour charger installer la bibliothèque, en python, plusieurs possibilités sont données sur le site web officiel. Vous pouvez utiliser pip
ou conda
, avec ou sans cuda
. Pour R
, cela a déjà été fait à la session 2022 de finistR.
Un bon ouvrage pour démarrer : Deep Learning and Scientific Computing with R
torch
.
Appel des fonctions
En python
, un premier exemple serait déjà d’importer la bibliothèque et de créer un tensor
:
import torch
= torch.tensor(1)
t1 t1
Donnera en R
:
library(torch)
<- torch_tensor(1)
t1 t1
torch_tensor
1
[ CPUFloatType{1} ]
On comprend que la convention de nommage garde torch_
en préfix de toutes les fonctions implémentées.
Construction d’un réseau de neurone, exemple d’un ResNet
Chargement des données
On va utiliser torchvision
pour importer un jeu de données connu (des images annotées). On veut appliquer des transformations sur le jeu de test, mais hélas tout n’est pas encore implémenté. On commente les transformations pas encore implementées.
= transforms.Compose([
transform #transforms.Pad(4),
#transforms.RandomHorizontalFlip(),
#transforms.RandomCrop(32),
transforms.ToTensor()])
<- function(img) {
transform # img <- torchvision::transform_pad(img, 4) # pas implémenté
# img <- torchvision::transform_random_horizontal_flip(img) # pas implémenté
# img <- torchvision::transform_random_crop(img, 32) # bug sur la taille des images
<- torchvision::transform_to_tensor(img)
img return(img)
}
Voici le code pour charger les données en python
et R
:
= 1000
num_samples
= torchvision.datasets.CIFAR10(root="./data",
trainData =True,
train=transform,
transform=True)
download
= torchvision.datasets.CIFAR10(root="./data",
testData =False,
train=transforms.ToTensor())
transform
= torch.utils.data.DataLoader(
trainLoader =Subset(trainData, range(num_samples)),
dataset=256,
batch_size=True)
shuffle
= torch.utils.data.DataLoader(
testLoader =Subset(testData, range(num_samples)),
dataset=256,
batch_size=False) shuffle
= 1000
num_samples
<- torchvision::cifar10_dataset(
train_data root = "./data",
train = TRUE,
transform = transform,
download = TRUE
)
<- torchvision::cifar10_dataset(
test_data root = "./data",
train = FALSE,
transform = torchvision::transform_to_tensor
)
<- dataloader(
train_loader dataset = dataset_subset(train_data, 1:num_samples),
batch_size = 256,
shuffle = TRUE
)
<- dataloader(
test_loader dataset = dataset_subset(test_data, 1:num_samples),
batch_size = 256,
shuffle = FALSE
)
Construction d’un block residual
Pour définir notre block residual, on crée une classe torch.nn.Module
et on y définit deux méthodes : __init__
et forward
: qui hérite de torch.nn.Module
.
def align(num_in, num_out, stride):
if num_in != num_out or stride > 1:
return nn.Sequential(
nn.Conv2d(=3, stride=stride, padding=1, bias=False
num_in, num_out, kernel_size
),
nn.BatchNorm2d(num_out)
)else:
return lambda x: x
class ResBlock(nn.Module):
def __init__(self, num_in, num_out, stride):
super(ResBlock, self).__init__()
self.align = align(num_in, num_out, stride)
self.conv1 = nn.Conv2d(num_in, num_out, kernel_size=3,
=stride, padding=1, bias=False)
strideself.bn1 = nn.BatchNorm2d(num_out)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(num_out, num_out, kernel_size=3,
=1, padding=1, bias=False)
strideself.bn2 = nn.BatchNorm2d(num_out)
def forward(self, x):
= self.conv1(x)
o = self.bn1(o)
o = self.relu(o)
o = self.conv2(o)
o = self.bn2(o)
o = o + self.align(x)
o = self.relu(o)
o return o
De la même manière, on peut créer un objet nn_module
en R
et y spécifier init
et forward
:
<- function(num_in, num_out, stride) {
align if (num_in != num_out || stride > 1) {
return(nn_sequential(
nn_conv2d(
num_in, num_out,kernel_size = 3, stride = stride, padding = 1,
bias = FALSE
),nn_batch_norm2d(num_out)
))else {
} return(function(x) x)
}
}
<- nn_module(
res_block initialize = function(num_in, num_out, stride) {
$align <- align(num_in, num_out, stride)
self$conv1 <- nn_conv2d(num_in, num_out,
selfkernel_size = 3,
stride = stride, padding = 1, bias = FALSE
)$bn1 <- nn_batch_norm2d(num_out)
self$relu <- nn_relu(inplace = TRUE)
self$conv2 <- nn_conv2d(num_out, num_out,
selfkernel_size = 3,
stride = 1, padding = 1, bias = FALSE
)$bn2 <- nn_batch_norm2d(num_out)
self
},forward = function(x) {
<- self$conv1(x)
o <- self$bn1(o)
o <- self$relu(o)
o <- self$conv2(o)
o <- self$bn2(o)
o <- o + self$align(x)
o <- self$relu(o)
o return(o)
} )
Constructeur ResNet
Pour construire notre ResNet, on veut créer des block residuals en chaîne. En python
, on le fait de la manière suivante (toujours en utilisant torch.nn.Module
) :
def buildResBlocks(num_in, num_out, stride, num_blocks):
= [ResBlock(num_in, num_out, stride)]
blocks for _ in range(1, num_blocks):
1))
blocks.append(ResBlock(num_out, num_out, return nn.Sequential(*blocks)
class ResNet(nn.Module):
def __init__(self, num_classes):
super(ResNet, self).__init__()
self.blocks0 = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=3,
=1, padding=1, bias=False
stride
),16),
nn.BatchNorm2d(=True)
nn.ReLU(inplace
)self.blocks1 = buildResBlocks(16, 16, 1, 2)
self.blocks2 = buildResBlocks(16, 32, 2, 2)
self.blocks3 = buildResBlocks(32, 64, 2, 2)
self.avgpool = nn.AvgPool2d(8)
self.fc = nn.Linear(64, num_classes)
def forward(self, x):
= x.shape[0]
n = self.blocks0(x)
o = self.blocks1(o)
o = self.blocks2(o)
o = self.blocks3(o)
o = self.avgpool(o)
o = self.fc(o.reshape(n, -1))
o return o
En R
, cela donne :
<- function(num_in, num_out, stride, num_blocks) {
build_res_blocks <- list(res_block(num_in, num_out, stride))
blocks for (i in 2:num_blocks) {
<- res_block(num_out, num_out, 1)
blocks[[i]]
}return(do.call(nn_sequential, blocks))
}
<- nn_module(
res_net initialize = function(num_classes) {
$blocks0 <- nn_sequential(
selfnn_conv2d(
3,
16,
kernel_size = 3,
stride = 1,
padding = 1,
bias = FALSE
),nn_batch_norm2d(16),
nn_relu(inplace = TRUE)
)$blocks1 <- build_res_blocks(16, 16, 1, 2)
self$blocks2 <- build_res_blocks(16, 32, 2, 2)
self$blocks3 <- build_res_blocks(32, 64, 2, 2)
self$avgpool <- nn_avg_pool2d(kernel_size = 8)
self$fc <- nn_linear(64, num_classes)
self
},forward = function(x) {
<- dim(x)[1]
n <- self$blocks0(x)
o <- self$blocks1(o)
o <- self$blocks2(o)
o <- self$blocks3(o)
o <- self$avgpool(o)
o <- torch_flatten(o, start_dim = 2)
o <- self$fc(o)
o return(o)
} )
Instanciation modèle et optimiseurs
Partie un peu plus rapide et simple, quasi identique dans les deux cas :
= "cpu"
device = ResNet(10).to(device)
model = torch.optim.Adam(model.parameters(), lr=0.001) optimizer
= "cpu"
device <- res_net$new(10)$to(device = device)
model model
An `nn_module` containing 195,738 parameters.
── Modules ─────────────────────────────────────────────────────────────────────
• blocks0: <nn_sequential> #464 parameters
• blocks1: <nn_sequential> #9,344 parameters
• blocks2: <nn_sequential> #37,184 parameters
• blocks3: <nn_sequential> #148,096 parameters
• avgpool: <nn_avg_pool2d> #0 parameters
• fc: <nn_linear> #650 parameters
<- optim_adam(model$parameters, lr = 0.001)
optimizer optimizer
<optim_adam>
Inherits from: <torch_optimizer>
Public:
add_param_group: function (param_group)
clone: function (deep = FALSE)
defaults: list
initialize: function (params, lr = 0.001, betas = c(0.9, 0.999), eps = 1e-08,
load_state_dict: function (state_dict, ..., .refer_to_state_dict = FALSE)
param_groups: list
state: State, R6
state_dict: function ()
step: function (closure = NULL)
zero_grad: function ()
Private:
step_helper: function (closure, loop_fun)
Apprentissage
def train():
for epoch in range(1, 11):
for i, (x, y) in enumerate(trainLoader):
= x.to(device), y.to(device)
(x, y) = model(x)
o = F.cross_entropy(o, y)
loss
optimizer.zero_grad()
loss.backward()
optimizer.step()if i % 100 == 0:
print("Epoch: {}\tLoss: {}".format(epoch, loss.item()))
<- function() {
train for (epoch in 1:10) {
<- 0
i ::loop(for (batch in train_loader) {
coro<- batch[[1]]
x <- batch[[2]]
y <- model(x)
o <- nnf_cross_entropy(o, y)
loss
$zero_grad()
optimizer$backward()
loss$step()
optimizerif (i %% 100 == 0) {
cat(sprintf("Epoch: %d\tLoss: %.4f\n", epoch, loss$item()))
}<- i + 1
i
})
} }
Comparaison temps de calcul
= time.time()
tic
train()print(time.time()-tic, "s")
<- system.time(
tic train()
)
Epoch: 1 Loss: 2.3799
Epoch: 2 Loss: 2.0263
Epoch: 3 Loss: 1.8510
Epoch: 4 Loss: 1.7074
Epoch: 5 Loss: 1.6638
Epoch: 6 Loss: 1.6062
Epoch: 7 Loss: 1.5259
Epoch: 8 Loss: 1.3865
Epoch: 9 Loss: 1.3425
Epoch: 10 Loss: 1.2090
tic
user system elapsed
44.978 3.288 31.441
Sur mon ordinateur : - python
: 27.6s (elapsed) - R
: 33.1s
Des temps de calcul très comparables !
Précision
Pour tester la précision :
= 0, 0
n, N with torch.no_grad():
for (x, y) in testLoader:
= x.to(device), y.to(device)
(x, y) = model(x)
o = torch.max(o, 1)
_, ŷ += y.size(0)
N += torch.sum(ŷ == y).item()
n print("Accuracy: {}".format(n/N))
with_no_grad({
<- 0
n_tests_ok <- 0
n_tests ::loop(for (batch in test_loader) {
coro<- batch[[1]]
x <- batch[[2]]
y <- model(x)
o <- torch_max(o, dim = 2)[[2]]
yest <- n_tests + y$shape
n_tests <- n_tests_ok + torch_sum(y == yest)$item()
n_tests_ok
})cat("Accuracy", n_tests_ok / n_tests, "\n")
})
Accuracy 0.395
Les deux codes donnent des résultats semblables !