인공지능 응용 공학

1990년대 말 CNN 응용의 길을 튼 LeCUN 교수의 LeNet-5 CNN 모델을 PyTorch 로 실습해보자.

coding art 2023. 8. 28. 18:59
728x90

 

 

LeNet-5

 

PyTorch 에서 은닉층이 없는 간단한 MNIST 문제를 다뤄 보자.

Colab 에서 !pip install ⦁⦁⦁ 명령에 의한 별도의 PyTorch 라이브러리 설치 없이 torch torchvision import 하여 사용할 수 있다.

 

1 # Simple Network MNIST
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision

 

torch import 되면 torch.nn torch.optim 을 사용할 수 있으며, torchvision import 되면 이를 상속하여 datasets.MNIST를 사용할 수 있다.

60,000개의 데이터중 50,000개는 학습용이며 10,000개는 검증용이다.

 

2 i# Load MNIST data
train = torchvision.datasets.MNIST('data', train=True, download=True)
test = torchvision.datasets.MNIST('data', train=False, download=True)
print(train.data.shape, train.targets.shape)
print(test.data.shape, test.targets.shape)
____________________________________________________________________
torch.Size([60000, 28, 28]) torch.Size([60000])
torch.Size([10000, 28, 28]) torch.Size([10000])

 

각각의 데이터 사이즈는 (28, 28) 이므로 flatten 하여 X_train 즉 일차원 어레이 리스트로 변환하여 정규화 한다. y_train 은 라벨값을 지정한다. 검증용도 마찬가지로 처리하자.

 

3 X_train = train.data.reshape(-1, 784).float() / 255.0
y_train = train.targets
X_test = test.data.reshape(-1, 784).float() / 255.0
y_test = test.targets

 

class Baseline 을 선언하자. 외부에서 선언된 class nn.Module 을 인수로 입력한다.

메서드 init 에서 self 변수들을 설정하여 초기화하도록 하자.

가장 먼저 특별하게 초기화해야 할 것은 외부에서 입력되는 즉 nn.Module 로서 super__init__() 으로 처리한다. 이는 nn.Module 내부에서 초기화가 이루어지지 않는다는 의미를 포함한다.

self 변수들 즉 랜덤수로 채워진 (784, 784) self.layer1, ReLU 활성화 함수를 사용하는 self.act, s랜덤수로 채워진 (784, 10) elf.layer2 로 초기화한다.

self 변수와 메인으로부터 입력 데이터 x를 인수로 하여 순전파 메서드 forward 를 선언하자.

self 변수와 입력 데이터 x를 사용하여 순전파 연산을 실행한다.

 

4 class Baseline(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(784, 784)
self.act1 = nn.ReLU()
self.layer2 = nn.Linear(784, 10)


def forward(self, x):
x = self.act1(self.layer1(x))
x = self.layer2(x)
return x

 

class Baseline model 로 설정하자.

optimizer SGD(stochastic gradient) 를 사용한다. model.parameters() 는 모든 웨이트 값들을 포함하고 있는 변수를 의미한다. learning rate 0.01 로 둔다.

Loss 함수는 Cross Entropy 를 사용한다.

batch_size 100 으로 둔다.

 

5 model = Baseline()


optimizer = optim.SGD(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
loader = torch.utils.data.DataLoader(list(zip(X_train, y_train)), shuffle=True, batch_size=100)

 

epoch 10 으로 설정하자. 하나의 epoch 연산이 완료되면 이때 얻어진 가중치들은 그 다음 epoch 단계에서 사용하여 정밀도를 올리게 된다.

지정된 epoch 담계에서 batch_size 100 으로 5000 번 반복 학습을 시킨다.

optimizer.zero_grad() 명령으로 기울기(gradient) 0.0 으로 초기화한다. 한편 Transfer Learning 의 경우 0.0 이 아닌 pretrained 값을 사용하는 경우를 참조하자.

순전파 연산이 완료되면 loss.backward() 명령에 의한 역전파 연산을 시행한다.

optimizer.step() 명령에 의해 스텝벼로 연산을 진행한다.

model.evaluate() 명령을 사용하여 정밀도를 출력한다.

 

6 n_epochs = 10
for epoch in range(n_epochs):
model.train()
for X_batch, y_batch in loader:
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Validation
model.eval()
y_pred = model(X_test)
acc = (torch.argmax(y_pred, 1) == y_test).float().mean()
print("Epoch %d: model accuracy %.2f%%" % (epoch, acc*100))

 

은닉층이 없는 Simple Network를 사용한 정밀도는 92.6% 가 상한이다.

 

7 Epoch 0: model accuracy 84.46%
Epoch 1: model accuracy 88.37%
Epoch 2: model accuracy 89.58%
Epoch 3: model accuracy 90.28%
Epoch 4: model accuracy 90.60%
Epoch 5: model accuracy 91.10%
Epoch 6: model accuracy 91.49%
Epoch 7: model accuracy 91.86%
Epoch 8: model accuracy 92.19%
Epoch 9: model accuracy 92.33%

 

PyTorch class CNN

transforms.Compose 명령에 의해 일단 입력 데이터를 Tensor 이미지로 변환 후 평균이 0.0 이고 표준편차가 128 인 조건 하에서 정규화를 시킨다.

batch_size 100 인 학습 데이터와 검증용 데이터를 준비한다.

 

8 # Simple CNN: Load MNIST data
transform = torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0,), (128,)),
])
train = torchvision.datasets.MNIST('data', train=True, download=True, transform=transform)
test = torchvision.datasets.MNIST('data', train=False, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(train, shuffle=True, batch_size=100)
testloader = torch.utils.data.DataLoader(test, shuffle=True, batch_size=100)

 

class nn.Module을 인수로 받아 들이는 class CNN 을 선언하자.

메서드 init 에서 self 변수들을 설정하여 초기화하도록 하자. 인수로 받는 nn.Modulesuper__init__() 으로 초기화 한다.

 

Conv2d( 1, 10, ⦁⦁⦁) 에서 양쪽으로 2개씩 0.0 으로 padding 하면 grayscale 이미지 ( ?, 28, 28, 1) 으로부터 10개의 특징 맵 ( ?, 32, 32, 10)을 얻을수 있다. 5x5 커늘을 사용, stride 1x1 을 적용하면 ( ?, 28,28, 10) 이 얻어진다.

ReLU 처리 시 shape 변화는 없다.

커늘 사이즈 2x2 stride=1 로 하여 Pool 처리하면 (?, 27, 27, 10) 을 얻는다.

Dropout(0.2) overfitting을 방지하기 위해 무작위로 20% 네트워크를 끊어 버린다.

Flatten()에 의해 (?, 27, 27, 10) (?, 27*27*10) 으로 shape을 바꾼다.

완전연결계층 처리를 위해 Linear 명령을 사용 27*27*10개에서 128개로 수를 줄인다.

shape 변화가 없는 ReLU 처리를 한다.

Linear 명령을 사용 128개에서 128개로 식별을 위한 class 수인 10으로 수를 줄인다.

 

9 class CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(1, 10, kernel_size=5, stride=1, padding=2)
self.relu1 = nn.ReLU()
self.pool = nn.MaxPool2d(kernel_size=2, stride=1)
self.dropout = nn.Dropout(0.2)
self.flat = nn.Flatten()
self.fc = nn.Linear(27*27*10, 128)
self.relu2 = nn.ReLU()
self.output = nn.Linear(128, 10)

 

self 변수들과 입력 데이터 x를 인수로 받아 순전파 연산을 위한 메서드 forward 를 선언한다.

앞서 초기화 과정에서 선언된 self 변수 명령들을 사용하여 순전파 연산이 이루어지며 최종 연산 결과 xinstance 값으로 반환한다.

 

 

100 def forward(self, x):
x = self.relu1(self.conv(x))
x = self.pool(x)
x = self.dropout(x)
x = self.relu2(self.fc(self.flat(x)))
x = self.output(x)
return x

 

class CNN을 불러 model 을 설정하자.

CNN에서 사용된 모든 가중치들을 의미하는 model_parameters()Adam optimizer 의 인수로 제공하고 Cross EntropyLoss 함수로 두자.

 

11 model = CNN()


optimizer = optim.Adam(model.parameters())
loss_fn = nn.CrossEntropyLoss()사용 27*27*10개에서 128개로 수를 줄인다.

 

epoch10회로 설정한다.

epoch 별로 model을 한번씩 학습(train)하고 평가(evaluate)가 이루어진다. 즉 하나의 epoch 가 완료되면 그때의 가중치를 초기값으로 둔 상태에서 다시 학습(train)과 평가(evaluate)가 이루어지게 된다. 따라서 epoch를 거듭할수록 정밀도가 좋아지게 된다.

Loss 함수 값은 X_batch 데이터를 사용한 순전파 연산된 결과인 y_pred 와 주어진 라벨값인 y_batch 와의 오차를 가지고 Cross Entropy 함수를 준비한다.

단 매회의 epoch 에서 학습시 Shuffling 에 의한 데이터 준비가 이루어져 있으며, 단지 기울기 값은 0.0 으로 초기화 된다.

역전파 연산을 실행한다.

optimizer 로 하여금 step 별로 가중치를 업데이트하여 학습을 진행한다.

 

12 n_epochs = 10
for epoch in range(n_epochs):
model.train()
for X_batch, y_batch in trainloader:
y_pred = model(X_batch)
loss = loss_fn(y_pred, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()

 

아래와 같이 인식률이 얻어지지만 Conv2d 의 파라메터들이 최적화 되지 않았기 때문에 인식률이 그다지 높지는 않다.

 

13 Epoch 0: model accuracy 76.12%
Epoch 1: model accuracy 84.81%
Epoch 2: model accuracy 87.05%
Epoch 3: model accuracy 88.06%
Epoch 4: model accuracy 88.79%
Epoch 5: model accuracy 88.96%
Epoch 6: model accuracy 89.51%
Epoch 7: model accuracy 90.03%
Epoch 8: model accuracy 90.34%
Epoch 9: model accuracy 90.69%

 

LeNet-5 코드를 살펴보자 . PyTorch class CNN 과 유사한 구조이지만 파라메터 설정에서 차이가 나며 보다 최적화 되어 있다.

첫 번째 Conv2d( 1, 6, ⦁⦁⦁) 에서 양쪽으로 2개씩 0.0 으로 padding 하면 grayscale 이미지 ( ?, 28, 28, 1) 으로부터 6개의 특징 맵 ( ?, 32, 32, 6) 이 얻어지는데, 5x5 커늘 사용, stride 1 을 적용하면 ( ?, 28, 28, 10) 이 얻어진다.

활성화 함수는 tanh을 사용한다. 1990년대에는 ReLU 가 사용되지 않았다.

커늘 사이즈 2x2 stride=2 로 하여 AvgPool 처리하면 (?, 14, 14, 6) 을 얻는다.

두 번째 Conv2d( 6, 16, ⦁⦁⦁) 에서 padding=0 이면 이미지 ( ?, 14, 14, 6) 으로부터 16개의 특징 맵 ( ?, 14, 14, 16) 이 얻어지는데, 커늘은 5x5 커늘 사용, stride 1 을 적용하면 ( ?, 10, 10, 16) 이 얻어진다.

활성화 함수는 tanh을 사용한다. shape 에 변화가 없다.

커늘 사이즈 2x2 stride=2 로 하여 AvgPool 처리하면 (?, 5, 5, 16) 을 얻는다.

세 번째 Conv2d( 16, 120, ⦁⦁⦁) 에서 padding=0 이면 이미지 ( ?, 5, 5, 16) 으로부터 120개의 특징 맵 ( ?, 5 5, 120) 이 얻어지는데, 5x5 커늘 사용, stride 1 을 적용하여 AvgPooling 하면 ( ?, 1, 1, 120) 이 얻어진다.

활성화 함수는 tanh을 사용한다. shape 에 변화가 없다.

마지만 뉴럴 계층에서는 더 이상 AvgPool 적용이 없으며 Flatten 명령을 적용, shape(120,)를 가지는 결과물을 얻어낸다..

 

Linear 명령을 사용 120개에서 84개로 수를 줄인다.

활성화 함수 tanh 을 적용한다. shape 에 변화가 없다.

Linear 명령을 사용 84개에서 MNIST class 수인 10개로 수를 줄인다.

 

14 class LeNet5(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2)
self.act1 = nn.Tanh()
self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0)
self.act2 = nn.Tanh()
self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)


self.conv3 = nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0)
self.act3 = nn.Tanh()
self.flat = nn.Flatten()
self.fc1 = nn.Linear(1*1*120, 84)
self.act4 = nn.Tanh()
self.fc2 = nn.Linear(84, 10)

 

self 변수들과 입력 데이터 x를 인수로 받아 순전파 연산을 위한 메서드 forward 를 선언한다.

앞서 초기화 과정에서 선언된 self 변수 명령들을 사용하여 순전파 연산이 이루어지며 최종 연산 결과 xinstance 값으로 반환한다.

 

15 def forward(self, x):
x = self.act1(self.conv1(x)) # input 1x28x28, output 6x28x28
x = self.pool1(x) # input 6x28x28, output 6x14x14
x = self.act2(self.conv2(x)) # input 6x14x14, output 16x10x10
x = self.pool2(x) # input 16x10x10, output 16x5x5
x = self.act3(self.conv3(x)) # input 16x5x5, output 120x1x1
x = self.act4(self.fc1(self.flat(x))) # input 120x1x1, output 84
x = self.fc2(x) # input 84, output 10
return x

 

class LeNet5()를 실행하기 위한 코드는 앞의 class CNN() 과 동일하므로 결과만 얻어보자. 상당히 높은 거의 98%에 육박하는 좋은 결과가 얻어졌다.

 

16 Epoch 0: model accuracy 89.65%
Epoch 1: model accuracy 92.86%
Epoch 2: model accuracy 94.24%
Epoch 3: model accuracy 95.72%
Epoch 4: model accuracy 96.11%
Epoch 5: model accuracy 96.34%
Epoch 6: model accuracy 96.61%
Epoch 7: model accuracy 96.83%
Epoch 8: model accuracy 97.16%
Epoch 9: model accuracy 97.18%

 

참조: Handwritten Digit Recognition with LeNEt5 Model in PyTorch

이 참고 블로그에서 MNIST 데이터를 읽어 들이는 코드 한 줄에 고의적인 error 가 있을 수 있으므로 사용자가 알아서 수정하도록 하자.

https://machinelearningmastery.com/handwritten-digit-recognition-with-lenet5-model-in-pytorch/

 

첨부된 파일을 다운받아 Colab 에서 GPU 를 사용하여 실행해 보자.

LeNet.ipynb
0.01MB