또르르's 개발 Story

[38-3] Pruning using PyTorch 본문

부스트캠프 AI 테크 U stage/실습

[38-3] Pruning using PyTorch

또르르21 2021. 3. 18. 02:21

Pruning을 수행하기 위한 간단한 모델을 생성하고 훈련합니다.

모델은 LeNet5, 데이터는 Mnist를 사용합니다.

 

https://pytorch.org/tutorials/intermediate/pruning_tutorial.html

 

자세한 내용은 Pytorch pruning tutorial를 참고해주세요.

 

Pruning Tutorial — PyTorch Tutorials 1.8.0 documentation

Note Click here to download the full example code Pruning Tutorial Author: Michela Paganini State-of-the-art deep learning techniques rely on over-parametrized models that are hard to deploy. On the contrary, biological neural networks are known to use eff

pytorch.org

 

1️⃣ layer weight 확인

 

생성된 모델의 첫 번째 레이어를 구성하는 parameter들은 다음과 같습니다.

첫 번째 weight와 bias로 구성됩니다.

module = model.conv1

print(list(module.named_parameters())) # will not change even after pruning

print(module.weight) # now it is the same as module.named_parameters()
[('weight', Parameter containing:
tensor([[[[ 0.1672, -0.1517, -0.1711],
          [ 0.1280, -0.1407,  0.3215],
          [-0.1074,  0.2512,  0.2103]]],


        [[[ 0.0602,  0.2026,  0.0181],
          [ 0.1411,  0.0426,  0.1177],
          [-0.0267,  0.1855,  0.0767]]],


        [[[ 0.3355,  0.0562, -0.5521],
          [-0.1873,  0.0618, -0.2184],
          [-0.0807,  0.2019,  0.4228]]],


        [[[ 0.3663, -0.0216,  0.2986],
          [ 0.1475,  0.4550,  0.3350],
          [-0.6378, -0.6662, -0.2658]]],


        [[[ 0.2871,  0.1041,  0.2666],
          [ 0.0158,  0.4709, -0.2112],
          [ 0.4000,  0.0856, -0.3989]]],


        [[[ 0.4908,  0.0509,  0.2348],
          [-0.3349, -0.2446, -0.7014],
          [-0.6735, -0.3921, -0.2518]]]], requires_grad=True)), ('bias', Parameter containing:
tensor([ 0.1874,  0.0468,  0.0102, -0.1306,  0.2191,  0.4025],
       requires_grad=True))]
Parameter containing:
tensor([[[[ 0.1672, -0.1517, -0.1711],
          [ 0.1280, -0.1407,  0.3215],
          [-0.1074,  0.2512,  0.2103]]],


        [[[ 0.0602,  0.2026,  0.0181],
          [ 0.1411,  0.0426,  0.1177],
          [-0.0267,  0.1855,  0.0767]]],


        [[[ 0.3355,  0.0562, -0.5521],
          [-0.1873,  0.0618, -0.2184],
          [-0.0807,  0.2019,  0.4228]]],


        [[[ 0.3663, -0.0216,  0.2986],
          [ 0.1475,  0.4550,  0.3350],
          [-0.6378, -0.6662, -0.2658]]],


        [[[ 0.2871,  0.1041,  0.2666],
          [ 0.0158,  0.4709, -0.2112],
          [ 0.4000,  0.0856, -0.3989]]],


        [[[ 0.4908,  0.0509,  0.2348],
          [-0.3349, -0.2446, -0.7014],
          [-0.6735, -0.3921, -0.2518]]]], requires_grad=True)

module ( model의 첫 번째 layer)의 named_buffer는 비어있는 것을 알 수 있습니다.

pruning 후에 named_buffer에 값이 채워질 것입니다.

# Check named_buffer is empty

print(list(module.named_buffers()))  # now the buffer is empty. It will show up after being pruned.

original output은 다음과 같습니다.

output_orig = model(images[:1])


>>> print(f"original output: {output_orig}")

original output: tensor([[ -7.9678, -13.6928, -20.6445,  -5.8569, -13.8743,  22.2749,  -2.0126,
         -13.3453,  -4.1711,  -4.2301]], grad_fn=<AddmmBackward>)

 

 

2️⃣ Random Unstructured Pruning => Weight

 

Pytorch에서는 여러 가지 prune method를 제공합니다.

여기서는 random으로 선택하는 방식을 적용해서 weight를 pruning 합니다.

 

torch의 prune module을 불러옵니다.

import torch.nn.utils.prune as prune

 

Random으로 Unstructured Pruning을 수행하기 위해 prune.random_unstructured을 불러옵니다.

이때, 첫 번째 layer (module)에만 적용하고, amount=0.3 비율로 수행합니다.

prune.random_unstructured(module, name="weight", amount=0.3)

 

named_parameters()를 찍어보면 weight -> weight_orig으로 이름이 바뀐 것을 알 수 있습니다.

>>> print(list(module.named_parameters()))
[('bias', Parameter containing:
tensor([ 0.1874,  0.0468,  0.0102, -0.1306,  0.2191,  0.4025],
       requires_grad=True)), ('weight_orig', Parameter containing:
tensor([[[[ 0.1672, -0.1517, -0.1711],
          [ 0.1280, -0.1407,  0.3215],
          [-0.1074,  0.2512,  0.2103]]],


        [[[ 0.0602,  0.2026,  0.0181],
          [ 0.1411,  0.0426,  0.1177],
          [-0.0267,  0.1855,  0.0767]]],


        [[[ 0.3355,  0.0562, -0.5521],
          [-0.1873,  0.0618, -0.2184],
          [-0.0807,  0.2019,  0.4228]]],


        [[[ 0.3663, -0.0216,  0.2986],
          [ 0.1475,  0.4550,  0.3350],
          [-0.6378, -0.6662, -0.2658]]],


        [[[ 0.2871,  0.1041,  0.2666],
          [ 0.0158,  0.4709, -0.2112],
          [ 0.4000,  0.0856, -0.3989]]],


        [[[ 0.4908,  0.0509,  0.2348],
          [-0.3349, -0.2446, -0.7014],
          [-0.6735, -0.3921, -0.2518]]]], requires_grad=True))]

 

그리고 아까 아무 값도 없던 named_buffers에서는 값이 들어가 있는 것을 확인할 수 있습니다.

>>> print(list(module.named_buffers()))
[('weight_mask', tensor([[[[0., 1., 0.],
          [0., 0., 1.],
          [0., 1., 1.]]],


        [[[1., 1., 1.],
          [1., 0., 1.],
          [1., 0., 0.]]],


        [[[0., 1., 0.],
          [1., 0., 1.],
          [1., 1., 1.]]],


        [[[1., 1., 1.],
          [0., 1., 1.],
          [1., 1., 0.]]],


        [[[1., 1., 1.],
          [1., 1., 1.],
          [0., 1., 1.]]],


        [[[1., 1., 1.],
          [1., 1., 1.],
          [1., 0., 0.]]]]))]

 

prune 함수를 수행하고 나면, 레이어의 named_buffers()에 'weight_mask'라는 새로운 파라미터가 하나 생성된 것을 볼 수 있습니다.

 

Pytorch에서는 mask tensor를 새로 생성해서 pruning을 수행하게 됩니다.

즉, 삭제할 값을 0, 보존할 값을 1로 설정한 mask tensor를 생성하고, 실제 계산을 수행할 때 mask와 weight를 element별로 곱한 tensor를 활용합니다.

 

이번에는 original model의 출력값과 첫 번째 레이어(module)를 가지치기한 모델의 결과를 비교해봅시다.

값에 차이가 생긴 것을 볼 수가 있습니다.

output_pruned_conv1 = model(images[:1])

>>> print(f"original output: {output_orig}")

>>> print(f"pruned weight output: {output_pruned_conv1}")

>>> print(f"difference: {output_orig - output_pruned_conv1}")
original output: tensor([[ -7.9678, -13.6928, -20.6445,  -5.8569, -13.8743,  22.2749,  -2.0126,
         -13.3453,  -4.1711,  -4.2301]], grad_fn=<AddmmBackward>)
         
pruned weight output: tensor([[ -8.6187, -12.7903, -18.8316,  -4.4383, -12.1617,  19.8098,  -3.0493,
         -11.2096,  -3.9549,  -3.8261]], grad_fn=<AddmmBackward>)
         
difference: tensor([[ 0.6509, -0.9025, -1.8129, -1.4185, -1.7127,  2.4650,  1.0368, -2.1357,
         -0.2162, -0.4040]], grad_fn=<SubBackward0>)

 

"Original model"과 "random_unstructured pruning을 적용한 model"의 차이는 다음과 같습니다.

Original model보다 Accuracy는 떨어졌고, Average loss가 오른 것을 알 수 있습니다.

(여기서 Elapsed time은 약간 늘어난 것을 볼 수 있는데 mask 계산이 추가되어 그런 것으로 짐작할 수 있습니다.)

 

 

 

3️⃣ L1 Unstructured Pruning => bias

 

이번에는 첫 번째 layer의 bias를 pruning합니다.

Random으로 값을 선택하여 pruning 한 weight와는 다르게, 값이 작은 순서대로 (L1 norm) pruning을 수행합니다.

 

이때, amount를 float으로 입력하면 해당 비율만큼, int로 입력할 경우 해당 개수만큼 값을 선택하여 pruning 합니다. Lowest L1-norm을 값 비교의 기준으로 삼게 됩니다.

prune.l1_unstructured(module, name="bias", amount=3)  

 

named_parameter는 다음과 같습니다.

l1_unstructured pruning을 적용하자 'bias' -> 'bias_orig'로 바뀐 것을 알 수 있습니다.

>>> print(list(module.named_parameters()))
[('weight_orig', Parameter containing:
tensor([[[[ 0.1672, -0.1517, -0.1711],
          [ 0.1280, -0.1407,  0.3215],
          [-0.1074,  0.2512,  0.2103]]],


        [[[ 0.0602,  0.2026,  0.0181],
          [ 0.1411,  0.0426,  0.1177],
          [-0.0267,  0.1855,  0.0767]]],


        [[[ 0.3355,  0.0562, -0.5521],
          [-0.1873,  0.0618, -0.2184],
          [-0.0807,  0.2019,  0.4228]]],


        [[[ 0.3663, -0.0216,  0.2986],
          [ 0.1475,  0.4550,  0.3350],
          [-0.6378, -0.6662, -0.2658]]],


        [[[ 0.2871,  0.1041,  0.2666],
          [ 0.0158,  0.4709, -0.2112],
          [ 0.4000,  0.0856, -0.3989]]],


        [[[ 0.4908,  0.0509,  0.2348],
          [-0.3349, -0.2446, -0.7014],
          [-0.6735, -0.3921, -0.2518]]]], requires_grad=True)), ('bias_orig', Parameter containing:
tensor([ 0.1874,  0.0468,  0.0102, -0.1306,  0.2191,  0.4025],
       requires_grad=True))]

 

이에 따라 named_buffers()에도 bias_mask가 생성되었습니다.

>>> print(list(module.named_buffers()))
[('weight_mask', tensor([[[[0., 1., 0.],
          [0., 0., 1.],
          [0., 1., 1.]]],


        [[[1., 1., 1.],
          [1., 0., 1.],
          [1., 0., 0.]]],


        [[[0., 1., 0.],
          [1., 0., 1.],
          [1., 1., 1.]]],


        [[[1., 1., 1.],
          [0., 1., 1.],
          [1., 1., 0.]]],


        [[[1., 1., 1.],
          [1., 1., 1.],
          [0., 1., 1.]]],


        [[[1., 1., 1.],
          [1., 1., 1.],
          [1., 0., 0.]]]])), ('bias_mask', tensor([1., 0., 0., 0., 1., 1.]))]

 

l1_unstructed pruning을 적용한 model의 performance는 다음과 같습니다.

device = 'cpu'

test(model=model, device=device, test_loader=test_loader)
LeNet(
  (conv1): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(3, 3), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)
========================================= PERFORMANCE =============================================
Size of the model(MB): 0.244443

Test set: Average loss: 0.1904, Accuracy: 9385/10000 (94%)

Elapsed time = 1.7257 milliseconds
====================================================================================================

 

"l1 unstructured pruning model"은 "Original model"과 "random unstructured pruning model"보다  

더 Accuracy는 떨어졌고, Average loss가 훨씬 오른 것을 알 수 있습니다.

(원래 Elapsed time은 떨어져야 하는 것이 맞지만 mask tensor 연산으로 인해 높아졌으며, pruning을 영구 적용하면 작아집니다.)

 

 

4️⃣ Pruning 영구 적용

 

Pruning을 수행한 후 더 이상 원본 파라미터가 필요하지 않을 때, pruning을 original model에 영구히 적용하는 방법을 알아봅시다.

 

prune.remove 함수를 사용하면, 기존의 원본 파라미터를 삭제하고 이를 prune이 적용된 weight를 교체하게 됩니다.

prune.remove(module, 'weight')			# (layer, pruning 이름)

 

named_parameter를 확인해보면 'weight_orig' => 'weight'로 바뀐 것을 알 수 있으며, 변경된 weight값이 적용됩니다.

>>> print(list(module.named_parameters()))
[('bias_orig', Parameter containing:
tensor([ 0.1874,  0.0468,  0.0102, -0.1306,  0.2191,  0.4025],
       requires_grad=True)), ('weight', Parameter containing:
tensor([[[[ 0.0000, -0.1517, -0.0000],
          [ 0.0000, -0.0000,  0.3215],
          [-0.0000,  0.2512,  0.2103]]],


        [[[ 0.0602,  0.2026,  0.0181],
          [ 0.1411,  0.0000,  0.1177],
          [-0.0267,  0.0000,  0.0000]]],


        [[[ 0.0000,  0.0562, -0.0000],
          [-0.1873,  0.0000, -0.2184],
          [-0.0807,  0.2019,  0.4228]]],


        [[[ 0.3663, -0.0216,  0.2986],
          [ 0.0000,  0.4550,  0.3350],
          [-0.6378, -0.6662, -0.0000]]],


        [[[ 0.2871,  0.1041,  0.2666],
          [ 0.0158,  0.4709, -0.2112],
          [ 0.0000,  0.0856, -0.3989]]],


        [[[ 0.4908,  0.0509,  0.2348],
          [-0.3349, -0.2446, -0.7014],
          [-0.6735, -0.0000, -0.0000]]]], requires_grad=True))]

 

또한, weight에 해당하는 mask 또한 없어진 것을 볼 수 있습니다.

>>> print(list(module.named_buffers()))
[('bias_mask', tensor([1., 0., 0., 0., 1., 1.]))]

 

마찬가지로 bias에 해당하는 prune도 영구 적용합니다.

prune.remove(module, 'bias')

 

named_parameter를 확인하면 bias도 다시 'bias_orig' => 'bias'로 변경된 것을 알 수 있습니다.

>>> print(list(module.named_parameters()))
[('weight', Parameter containing:
tensor([[[[ 0.0000, -0.1517, -0.0000],
          [ 0.0000, -0.0000,  0.3215],
          [-0.0000,  0.2512,  0.2103]]],


        [[[ 0.0602,  0.2026,  0.0181],
          [ 0.1411,  0.0000,  0.1177],
          [-0.0267,  0.0000,  0.0000]]],


        [[[ 0.0000,  0.0562, -0.0000],
          [-0.1873,  0.0000, -0.2184],
          [-0.0807,  0.2019,  0.4228]]],


        [[[ 0.3663, -0.0216,  0.2986],
          [ 0.0000,  0.4550,  0.3350],
          [-0.6378, -0.6662, -0.0000]]],


        [[[ 0.2871,  0.1041,  0.2666],
          [ 0.0158,  0.4709, -0.2112],
          [ 0.0000,  0.0856, -0.3989]]],


        [[[ 0.4908,  0.0509,  0.2348],
          [-0.3349, -0.2446, -0.7014],
          [-0.6735, -0.0000, -0.0000]]]], requires_grad=True)), ('bias', Parameter containing:
tensor([0.1874, 0.0000, 0.0000, -0.0000, 0.2191, 0.4025], requires_grad=True))]

 

더 이상 named_buffer에는 아무 값도 남아있지 않게 됩니다.

>>> print(list(module.named_buffers()))
[]

'부스트캠프 AI 테크 U stage > 실습' 카테고리의 다른 글

[39-3] Teacher-Student Network using PyTorch  (0) 2021.03.19
[39-2] Quantization using PyTorch  (0) 2021.03.18
[38-2] Python 병렬 Processing  (0) 2021.03.17
[37-2] PyTorch profiler  (0) 2021.03.17
[36-1] Model Conversion  (0) 2021.03.16
Comments