Paper Info
논문 제목 : "Gradient-Based Learning Applied to Document Recognition"
저자 : LeCun, Y., Boser, B., Denker, J. S., Henderson, D., Howard, R. E., Hubbard, W., & Jackel, L. D.
연도 :1989
paper : http://vision.stanford.edu/cs598_spring07/papers/Lecun98.pdf
CNN Study Plan
CNN 분야의 발전에 큰 영향을 미친 다양한 연구들을 검토할 예정
model code : https://github.com/yeongjinHwang/CNN
- LeNet-5
paper review : https://gongjin-repository.tistory.com/67 - AlexNet : https://gongjin-repository.tistory.com/69
- VGGNet
- GoogLeNet
- ResNet
- DenseNet
- MobileNet
- EfficientNet
:: Background
LeNet-5는 손글씨 숫자 인식 문제를 해결하기 위해 제안된 최초의 CNN 모델 중 하나. 이 모델은 MNIST 데이터셋에서 높은 성능을 보여주며, CNN의 효용성을 입증했다.
1. LeNet-5 Architecture
Input : 32x32 흑백 image
C1 Layer : Convolution Layer
filter = 5x5 (@6), stride=1로 conv연산 따라서 (32-5)/2 +1=28, 6@28x28 feature map
(156 parameter, 122,306 connection을 갖는다고 한다.
156= (filter + bias) @6 = (5x5 +1)*6 = 156, 122,306 = (feature map)×(param) = 28*28*156
S2 Layer : Subsampling Layer
filter = 2x2 (@6), stride=2로 down sampling, 따라서 (28-2)/2 +1=14, 6@14x14 feature map
down samping은 average pooling을 통해 수행
Activation Function : Sigmoid
-> 신기하게도 이 논문 당시에는 avg. pooling이 대세였는가보다...?
[좌,우] = [Sigmoid, Relu]
왜 해당 논문에서는 Sigmoid를 사용했을까?
Back propagation은 논문에서 찾아볼 수 있으나 ReLU에 대한 언급은 없는것으로 보아 해당 논문 발행 당시 ReLU가 없었거나 ReLU가 유명하지 않았던 것으로 보인다.
만약, 현대에 나온 논문이라면, Acticvation Function으로 sigmoid보다는 ReLU를 사용했을 것이다.
Layer가 깊어지면 Back Propagation 과정에서 sigmoid는 Gradient Vanishing 현상으로 인해, 문제가 생길 수 있다.
하지만, ReLU의 경우 음수값이 0으로 기울기가 되기때문에 입력값이 음수것은 다시 살리기 힘들다.
-> 죽어가는 ReLU라는 별명 생긴 이유....
그렇기 때문에 Leakly ReLU를 사용하기도 한다.
위의 사진은 Forward Propagation 과정이다. weight와 bias의 계산을 통해 최종 Output(yhat)을 산출하고
yhat(예측값, 결과값)과 y(실제값)을 loss function의 input으로 하여 loss를 계산한다.
만약, 모든 data에 대해 Loss를 Cost Function에 input으로 한다면 해당 model의 Cost를 계산할 수 있다.
결론적으로 해당 과정이 모두 Forward Propagation이다.
Back Propagation이란 Forward Propation을 역으로 수행하는 과정이며,
Chain Role을 활용하고 Weight와 Bias를 Optimization(loss function 최소화)하는 것이다.
해당 논문에서는 Optimization을 Gradient Descent Algorithm(경사하강법)을 사용하였다.
다음과 같이 미분값이 최소화되는 weight를 찾아가는게 Optimization이다.
이때 Step을 결정하는 것이 중요한데 우선 해당 논문에서는 다음의 수식으로써 기본적인 방법으로 step을 결정지었다.
(Momentum)
해당 수식을 해석하자면,
다음으로 확인할 수 있습니다. (왼쪽 or 오른쪽 중 어디로 가야 Weight를 minima에 가깝게 Update할 수 있는지)
C3 Layer : Convolution Layer
stride=5로, 16@10x10 feature map
C3 Layer는 조금 독특하게 구성되어있다. 6개의 S2 feature map을 통해 C3 feature map 16개와 연결한다.
이는 위의 Table1을 따른다.
S2의 모든 feature map이 C3의 모든 feature map과 연결되지 않은 이유는
합리적인 범위의 연결의 수를 유지
신경망에서의 대칭 요구
서로 다른 feature map은 서로 다른 input set을 갖기에 다른 feature를 생성
S4 Layer : Subsampling Layer
filter : 2x2 (@16), stride : 2 -> 16@5x5 feature map
C1과 S2에서와 비슷한 방식으로 C3와 S4 연결
C5 Layer : Convolution Layer
filter : 5x5이므로 앞의 S4를 거치면 1x1의 feature map size, 120@1x1 feature map
input이 더 커지면 feature map 차원이 1ㅌ1보다 크기 때문에 FC Layer가 아닌 Convolutional Layer사용
F6 Layer : Fully-Connected Layer
output Layer를 ascii 각 문자 크기 7*12를 따르기 위해 84개로 units를 결정
loss function으로는 MSE를 사용했다.
Code
module import
import torch
import torch.nn as nn
from torchsummary import summary
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
model Definition
class LeNet5(nn.Module):
"""
LeNet-5 Model Architecture
Input :
1x32x32 binary image
Output :
10
"""
def __init__(self):
super(LeNet5, self).__init__()
self.pool = nn.AvgPool2d(kernel_size=2, stride=2) # Avg pooling layer (subsampling)
# Convolutional layers by LeNet-5 architecture
self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1, padding=0)
self.conv3 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0)
self.conv5 = nn.Conv2d(in_channels=16, out_channels=120, kernel_size=5, stride=1, padding=0)
# Fully connected layers
self.fc1 = nn.Linear(in_features=120, out_features=84)
self.fc2 = nn.Linear(in_features=84, out_features=10)
def forward(self, x):
x = torch.relu(self.conv1(x))
x = self.pool(x)
x = torch.relu(self.conv3(x))
x = self.pool(x)
x = torch.relu(self.conv5(x))
x = torch.flatten(x, 1) # Flatten the tensor to [batch_size, 120]
x = torch.relu(self.fc1(x))
x = self.fc2(x) # Final layer without activation for CrossEntropyLoss
return x
해당 논문에서는 sigmoid를 사용했으나 나는 ReLU를 사용했다.
Architecture를 기반으로 feature map을 똑같이 생성되도록 구성했으며,
fully-connected Layer 접근 전 1x120 size로 flatten(평탄화) 작업을 수행했다.
Config
if __name__ == '__main__':
# Check if GPU is available
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Model summary
model = LeNet5().to(device) # Create model and move to GPU if available
summary(model, input_size=(1, 32, 32)) # (channels, height, width)
# Initial weights
print("Initial Conv1 filters (weights):")
print(model.conv1.weight.data.cpu().numpy())
# Optimizer and loss function
learning_rate = 0.01
optimizer = optim.SGD(model.parameters(), lr=learning_rate) # Gradient Descent Optimizer
loss_function = nn.CrossEntropyLoss() # CrossEntropyLoss for multi-class classification
gpu 사용할 수 있으면 사용하도록 device 옮겨주었고, 최초 weight가 어떻게 설정되었는가 궁금해서 출력해보았다.
learning_rate=0.01, optimizer : Gradient Descent, loss_function : CrossEntropyLoss 사용했다.
nn.CrossEntropyLoss()는 내부적으로 LogSoftmax와 Negative Log Likelihood Loss를 결합한 것과 동일하다.
초기 Weight는 다음과 같이 6개가 생성된다. (C1 Layer에서 6@28x28 feature map을 생성하므로)
Data 준비
# Data Preparation
transform = transforms.Compose([
transforms.Resize((32, 32)), # Resize images to 32x32
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
# Load MNIST dataset
train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True) # 60,000 image
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform, download=True) # 10,000 image
# Create DataLoader for batch processing
train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)
MNIST에 적용하는 방법으로 수행했다.
image 전처리에 있어서는 MNIST의 경우 28x28 digit image이지만 현재 LeNet-5는 32x32 image를 input받도록 설계했기 때문에 Resize, Tensor로 변환했다.
Tensor 변환시 0~1의 범위로 정규화되는데 이를 mean=0.5, std=0.5 로써 중앙 정렬 정규화를 추가적으로 수행한다.
(0~1의 범위가 -1~1로 정규화된다)
MNIST를 통해 data를 받아와서 train 60,000, test 10,000개를 사용하며 batch_size=64로써 수행한다.
따라서 train의 경우 60000 / 64 번의 학습이 수행된다.
Train
# Training
num_epochs = 10
for epoch in range(num_epochs):
model.train() # Set to training mode
running_loss = 0.0
for images, labels in train_loader:
# Move data to the same device as the model (GPU if available)
images, labels = images.to(device), labels.to(device)
# Forward pass
outputs = model(images)
loss = loss_function(outputs, labels)
# Backward pass and optimization
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}")
# Weights after training
print("\nTrained Conv1 filters (weights):")
print(model.conv1.weight.data.cpu().numpy())
10 epoch로 총 6만개의 데이터를 64batch size로 10번 학습하도록 하였다.
현재 MNIST image가 cpu에 있으므로 gpu로 옮겨주고 image와 label(실제값)을 가져오고,
image로 model input하여 loss function을 통해 loss 계산 및 backpropagation을 통해 optimization한다.
C1 Layer의 Weight를 살펴보면 Back Propagation을 통해 Weight Update된 모습을 확인할 수 있다.
또한 epoch 마다 loss를 확인할 수 있다. 여기서, minibatch는 batch size 64이므로 64번마다 weight update가 수행된다.
따라서 총 weight update는 10epoch * (data==60000)/minibatch = 10*(60000/64) 번 수행된다.
Test
# Testing the model on two images for each digit from 0 to 9
model.eval() # Set to evaluation mode
# Select two images for each digit (0-9) from the test dataset
selected_images = []
selected_labels = []
count = {i: 0 for i in range(10)} # To keep track of how many images per digit are selected
for image, label in test_dataset:
if count[label] < 2: # Select only 2 images per digit
selected_images.append(image)
selected_labels.append(label)
count[label] += 1
if all(c == 2 for c in count.values()):
break
# Display the images and model predictions
plt.figure(figsize=(20, 10))
# Sort selected images by label
sorted_indices = sorted(range(len(selected_labels)), key=lambda i: selected_labels[i])
for i, idx in enumerate(sorted_indices):
image = selected_images[idx].unsqueeze(0).to(device) # Add batch dimension and move to GPU
label = selected_labels[idx]
output = model(image)
_, predicted = torch.max(output.data, 1)
# Plot the image
plt.subplot(4, 5, i + 1)
plt.imshow(image.cpu().squeeze(), cmap='gray') # Move back to CPU for plotting
plt.title(f'Label: {label}, Pred: {predicted.item()}')
plt.axis('off')
plt.show()
test를 수행하기 위해 evaluation mode로 변경해주고, test dataset에서 label을 통해 0~9까지의 image 2개씩 추출한다.
이를 model input으로 주어 예측값을 도출하고 실제값과 예측값, 예측이미지를 출력한다.
...
현재 기준 너무 오래된 논문이라. 해당 논문 발행당시 시대적 배경이나 어느 기술까지 개발이 되었는지에 대해 알지 못하여 아쉬웠다. 따라서 왜 x라는 알고리즘을 쓰지 않았을까? 라고 고민했을 때, x라는 알고리즘이 해당 논문 발행 당시 있었을까? 부터 찾아봐야돼서 어려웠다. 또한 CNN초창기 LeNet-5에 대한 논문이므로 간단하게 설명이 되어있을 줄 알았으나 46page로 너무 많은 양의 논문이었다.... 읽다가 지쳐서 아키텍쳐 기반으로 필수로 알아야되는 부분만 정리해두었다.
읽으면서 너무 오래된 논문이라 이해되지않는 기술 알고리즘 또는 수식이 딱히 없어서 이해하기에는 쉬웠다.
'Study > CNN' 카테고리의 다른 글
[VGGNet] Very Deep Convolutional Networks for Large-Scale Image Recognition (0) | 2024.08.12 |
---|---|
[AlexNet] ImageNet Classification with Deep ConvolutionalNeural Networks (0) | 2024.08.09 |
Background. 현재 나의 지식 (1) | 2024.08.07 |