Table of Contents
MLP
MLP는 "Multi-Layer Perceptron"의 약자로, 기본적인 형태의 인공 신경망을 말합니다. MLP는 하나 이상의 hidden layer를 포함할 수 있으며, 각 layer는 여러 개의 뉴런(또는 노드)으로 구성됩니다. MLP는 일반적으로 supervised learning, 즉 지도 학습 문제에 사용됩니다.
MLP의 주요 구성 요소
- Input Layer: 입력 데이터를 받는 첫 번째 층입니다. 각 뉴런은 데이터의 한 특성(feature)에 대응됩니다.
- Hidden Layers: 하나 이상 존재할 수 있는 중간 층으로, 복잡한 데이터 특징을 학습하는 데 사용됩니다. 각 hidden layer의 뉴런 수는 자유롭게 설정할 수 있으며, 이 수가 많을수록 모델은 더 복잡한 패턴을 학습할 수 있지만, 과적합(overfitting)의 위험이 커질 수 있습니다.
- Output Layer: 최종 예측을 수행하는 층입니다. 분류 문제의 경우, 출력 뉴런의 수는 분류하려는 클래스의 수와 일치합니다. 회귀 문제에서는 보통 하나의 출력 뉴런을 사용합니다.
작동 원리
- Forward Propagation: 입력 데이터는 input layer를 통해 네트워크에 들어가고, 각 hidden layer를 통과하면서 가중치와 바이어스에 의해 변환됩니다. 각 뉴런에서의 출력 값은 활성화 함수에 의해 결정됩니다.
- Activation Function: 뉴런의 출력을 결정하는 함수로, 비선형성을 추가하여 네트워크가 더 복잡한 패턴을 학습할 수 있도록 합니다. 일반적인 활성화 함수로는 Sigmoid, Tanh, ReLU 등이 있습니다.
- Backpropagation: 실제 출력과 기대 출력 사이의 오차를 계산하고, 이 오차를 최소화하는 방향으로 네트워크의 가중치를 조정합니다. 이 과정은 Gradient Descent 또는 그 변형을 사용하여 수행됩니다.
응용
MLP는 손글씨 인식, 음성 인식, 이미지 분류, 주가 예측 등 다양한 분야에서 사용됩니다. 하지만, 입력 데이터의 공간적, 시간적 구조를 고려하지 못하는 단점이 있어, 이러한 특성이 중요한 문제에서는 CNN이나 RNN과 같은 더 특화된 네트워크 아키텍처가 선호됩니다.
MLP 예시 코드
PyTorch를 사용하여 간단한 Multi-Layer Perceptron (MLP) 모델을 구현하는 예시 코드를 제공하겠습니다. 이 예제에서는 기본적인 MLP 구조를 사용하여 임의의 데이터에 대한 회귀 문제를 해결합니다. 모델은 두 개의 hidden layer를 포함하고 있으며, 각 layer에는 ReLU 활성화 함수를 사용합니다.
1. 필요한 라이브러리 임포트
먼저, 필요한 PyTorch 라이브러리를 임포트합니다.
import torch
import torch.nn as nn
import torch.optim as optim
2. MLP 모델 정의
nn.Module
을 상속받아 MLP 클래스를 정의합니다. 이 클래스는 입력층, 두 개의 은닉층, 그리고 출력층을 포함합니다.
class SimpleMLP(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleMLP, self).__init__()
# 첫 번째 은닉층
self.fc1 = nn.Linear(input_size, hidden_size)
# 두 번째 은닉층
self.fc2 = nn.Linear(hidden_size, hidden_size)
# 출력층
self.fc3 = nn.Linear(hidden_size, output_size)
def forward(self, x):
# ReLU 활성화 함수를 사용하는 첫 번째 은닉층
x = torch.relu(self.fc1(x))
# ReLU 활성화 함수를 사용하는 두 번째 은닉층
x = torch.relu(self.fc2(x))
# 출력층
x = self.fc3(x)
return x
3. 모델 인스턴스 생성 및 설정
모델을 초기화하고 손실 함수와 최적화 방법을 설정합니다.
# 모델 파라미터 설정
input_size = 10 # 입력 크기
hidden_size = 50 # 은닉층의 뉴런 수
output_size = 1 # 출력 크기
model = SimpleMLP(input_size, hidden_size, output_size)
# 손실 함수 및 최적화 방법
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
4. 학습 데이터 준비 및 학습 과정
임의의 데이터를 생성하여 모델을 학습시키는 과정입니다.
# 임의의 입력 데이터와 타깃 데이터
x_train = torch.randn(100, input_size) # (batch_size, input_size)
y_train = torch.randn(100, output_size) # (batch_size, output_size)
# 학습 과정
for epoch in range(200):
model.train()
optimizer.zero_grad()
output = model(x_train)
loss = criterion(output, y_train)
loss.backward()
optimizer.step()
if (epoch+1) % 20 == 0:
print(f'Epoch {epoch+1}, Loss: {loss.item()}')
이 코드는 PyTorch를 이용해 간단한 MLP를 구현하고, 생성한 가상 데이터로 모델을 학습시키는 방법을 보여줍니다. MLP는 여러 다양한 문제를 해결하기 위해 쉽게 조정하고 확장할 수 있으며, 활성화 함수, 레이어 수, 뉴런 수 등을 적절히 변경하여 다양한 응용 분야에 적용할 수 있습니다.
CNN
CNN (Convolutional Neural Network)은 특히 이미지와 같은 고차원 데이터를 처리하는 데 효율적인 신경망 아키텍처입니다. CNN은 MLP (Multi-Layer Perceptron)와 비교할 때 몇 가지 중요한 차이점이 있으며, 이러한 차이점은 CNN이 특히 시각적 데이터 처리에 적합하게 만듭니다.
CNN (Convolutional Neural Network)은 주로 이미지와 같은 고차원 데이터를 처리하는 데 특화된 딥러닝 아키텍처입니다. 이러한 신경망은 특히 시각적 데이터에서 패턴을 인식하고 이해하는 데 뛰어난 능력을 보여줍니다. CNN은 1980년대에 발전하기 시작하여, 1998년 Yann LeCun에 의해 소개된 LeNet-5 모델로부터 크게 발전하였고, 현재는 매우 다양한 분야에서 널리 사용되고 있습니다.
CNN의 주요 구성 요소
- Convolutional Layer: 이 층은 입력 데이터에 여러 필터를 적용하여 특징 맵(feature map)을 생성합니다. 필터는 일반적으로 작은 창(window)을 통해 입력 데이터를 슬라이딩하면서 각 위치에서의 합성곱 연산을 수행합니다. 이 과정을 통해 로컬 특징을 추출할 수 있습니다.
- ReLU Layer: Convolutional layer 다음에 보통 ReLU(Rectified Linear Unit) 활성화 함수가 적용됩니다. ReLU는 비선형 변환을 제공하여 네트워크가 비선형 문제를 해결할 수 있도록 돕습니다.
- Pooling Layer: 이 층은 특징 맵의 크기를 줄이면서 필수적인 정보만을 유지합니다. 풀링(pooling)은 일반적으로 최대 풀링(max pooling)과 평균 풀링(average pooling)의 두 가지 유형이 있으며, 과적합을 방지하고 계산 효율성을 높이는 역할을 합니다.
- Fully Connected Layer: CNN의 마지막 부분에서는 하나 이상의 fully connected layer가 사용됩니다. 이 층은 모든 특징을 종합하여 최종 분류나 회귀 출력을 생성합니다.
CNN의 작동 원리
CNN은 다음과 같은 과정을 통해 이미지나 다른 시각적 데이터를 처리합니다:
- 입력 이미지가 네트워크에 주어집니다.
- Convolutional layer를 통해 여러 필터가 적용되어 다양한 특징 맵이 생성됩니다.
- 생성된 특징 맵에 활성화 함수가 적용되어 비선형 변환을 수행합니다.
- Pooling layer를 통해 특징 맵의 크기가 줄어들고, 중요한 특징만이 강조됩니다.
- 마지막으로, 모든 특징이 종합되어 fully connected layer를 통해 최종 결과가 출력됩니다.
CNN의 응용
CNN은 다양한 분야에서 광범위하게 사용됩니다:
- 이미지 분류: 개와 고양이와 같은 객체를 이미지에서 분류합니다.
- 객체 인식 및 탐지: 이미지 내의 객체 위치를 식별하고 테두리를 그립니다.
- 자동차에서의 자율 주행: 도로 표지판 인식, 차선 탐지 등에 사용됩니다.
- 의료 이미징: MRI나 CT 스캔에서 이상 징후를 탐지합니다.
CNN은 그 효율성과 정확성 때문에 딥러닝과 인공 지능 분야에서 중추적인 역할을 계속해서 수행하고 있습니다.
CNN과 MLP의 주요 차이점
- 구조적 차이:
- MLP: 전통적인 MLP는 모든 뉴런이 이전 층의 모든 뉴런과 연결된 완전 연결(fully connected) 층으로 구성됩니다. 이는 데이터의 공간적인 구조를 유지하지 않습니다.
- CNN: CNN은 주로 convolutional layer, pooling layer, 그리고 fully connected layer의 조합으로 구성됩니다. Convolutional layer와 pooling layer는 이미지의 공간적 구조를 보존하면서 작동합니다.
- 파라미터 공유:
- MLP: 각 연결에 대해 고유한 가중치를 가지고 있습니다.
- CNN: Convolutional layer에서는 동일한 필터(가중치 집합)가 입력 데이터의 다양한 부분에 적용되어, 파라미터의 수를 크게 줄이고 효율성을 높입니다.
- 공간적 특성 인식:
- MLP: 입력의 공간적 관계를 인식하지 못합니다. 예를 들어, 이미지 내의 픽셀들 사이의 위치 관계가 입력 데이터에 반영되지 않습니다.
- CNN: Convolutional layer를 통해 입력 데이터의 로컬 패턴을 학습할 수 있습니다. 이는 이미지 내에서 유사한 패턴이 다른 위치에 나타나도 잘 인식할 수 있게 합니다.
- 적용 분야:
- MLP: 비교적 간단한 패턴 인식, 기본적인 분류 작업에 적합합니다.
- CNN: 이미지와 비디오 처리, 음성 인식, 자연어 처리에서 특히 뛰어난 성능을 보입니다. 이미지 분류, 객체 탐지, 시멘틱 세그멘테이션 등의 작업에서 주로 사용됩니다.
결론
CNN은 이미지 처리에 특화된 아키텍처로서, 공간적 특성을 인식하고, 파라미터 공유를 통해 효율적으로 대규모 이미지 데이터를 처리할 수 있습니다. 반면, MLP는 간단한 구조로 빠르게 구현할 수 있지만, 고차원 데이터의 공간적 특성을 활용하는 데는 한계가 있습니다. 따라서 특정 작업과 데이터 유형에 따라 적절한 모델을 선택하는 것이 중요합니다.
CNN 예시 코드
PyTorch를 사용하여 간단한 CNN (Convolutional Neural Network) 모델을 구현하는 예시 코드를 제공하겠습니다. 이 예제에서는 기본적인 CNN 구조를 사용하여 간단한 이미지 분류 작업을 수행합니다. 모델은 두 개의 convolutional layer를 포함하며, ReLU 활성화 함수와 max pooling을 사용합니다.
1. 필요한 라이브러리 임포트
먼저, 필요한 PyTorch 라이브러리와 기타 도구들을 임포트합니다.
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
2. CNN 모델 정의
nn.Module
을 상속받아 CNN 클래스를 정의합니다. 이 클래스는 입력층, 두 개의 convolutional layer, max pooling layer, 그리고 fully connected layer를 포함합니다.
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))
self.layer2 = nn.Sequential(
nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))
self.fc = nn.Linear(7*7*32, 10) # assuming input images are 28x28 pixels
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = out.view(out.size(0), -1) # flatten the output for the fully connected layer
out = self.fc(out)
return out
3. 모델 인스턴스 생성 및 설정
모델을 초기화하고, 손실 함수와 최적화 방법을 설정합니다.
model = SimpleCNN()
# 손실 함수 및 최적화 방법
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
4. 데이터 준비
간단한 데이터 로딩 및 전처리 작업입니다. 여기서는 MNIST 데이터셋을 사용합니다.
# MNIST 데이터 로드
train_dataset = torchvision.datasets.MNIST(root='./data',
train=True,
transform=transforms.ToTensor(),
download=True)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=64,
shuffle=True)
5. 학습 과정
모델을 학습시키는 과정입니다.
# 학습 과정
num_epochs = 5
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (i+1) % 100 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {loss.item():.4f}')
이 코드는 간단한 CNN을 구현하고, MNIST 이미지 데이터셋에 대해 모델을 학습시키는 과정을 보여줍니다. CNN은 다양한 이미지 처리 작업에 적용될 수 있으며, 위의 코드를 다른 데이터셋과 다른 이미지 분류 작업에 적용하기 위해 조정할 수 있습니다.
RNN
RNN (Recurrent Neural Network)은 특히 시퀀스 데이터를 처리하기 위해 설계된 신경망 아키텍처입니다. 이는 데이터의 시간적 순서와 연속성을 중요시하는 작업에 적합하며, 주로 자연어 처리(NLP), 음성 인식, 시계열 데이터 분석 등에 활용됩니다.
RNN의 핵심 특징
- 순환 구조: RNN의 주요 특징은 순환적인 구조를 가지고 있다는 것입니다. 이 구조는 네트워크의 출력을 다시 입력으로 사용함으로써 과거의 정보를 현재의 결정에 반영할 수 있습니다. 이를 통해 네트워크는 시퀀스의 시간적 연속성과 컨텍스트를 학습할 수 있습니다.
- 파라미터 공유: 모든 시간 단계에서 동일한 가중치를 사용합니다. 이는 모델의 파라미터 수를 크게 줄이며, 다양한 길이의 입력 데이터에 대한 학습과 적용이 가능하게 합니다.
- 동적 입력 및 출력 길이: RNN은 입력 시퀀스의 길이에 제한을 두지 않습니다. 이는 예를 들어 문장의 길이가 서로 다른 자연어 처리 작업에 매우 유용합니다.
RNN의 작동 원리
RNN은 각 시간 단계에서 현재 입력과 이전 시간 단계의 출력(또는 상태)을 결합하여 현재의 출력을 생성합니다. 이 과정은 다음과 같은 수식으로 표현될 수 있습니다:
[ h_t = f(W \cdot [h_{t-1}, x_t] + b) ]
여기서 ( h_t )는 시간 ( t )에서의 은닉 상태, ( x_t )는 시간 ( t )에서의 입력, ( W )와 ( b )는 네트워크의 가중치와 바이어스, ( f )는 비선형 활성화 함수입니다.
RNN의 한계
- 기울기 소실/폭발 문제: 긴 시퀀스를 처리할 때, 기울기가 너무 작아지거나 커져서 학습이 제대로 이루어지지 않는 문제가 발생할 수 있습니다. 이는 기울기가 순환 구조를 통해 역전파될 때 발생합니다.
- 장기 의존성 학습의 어려움: RNN은 이론적으로는 시간을 거슬러 정보를 전달할 수 있지만, 실제로는 긴 시퀀스에서 시간이 지날수록 초기 정보가 희미해지는 문제가 있습니다.
고급 RNN 아키텍처
위의 한계를 극복하기 위해 LSTM (Long Short-Term Memory)과 GRU (Gated Recurrent Unit)와 같은 고급 RNN 구조가 개발되었습니다. 이러한 구조들은 내부 게이트를 통해 정보를 보다 효율적으로 관리하고, 필요한 정보를 보존하면서 불필요한 정보는 제거하는 메커니즘을 사용합니다.
RNN과 이러한 변형들은 복잡한 시퀀스 데이터를 모델링하는 데 매우 효과적이며, 다양한 실세계 응용 분야에서 중요한 역할을 합니다.
RNN 예시 코드
PyTorch에서 간단한 RNN 모델을 구현하는 예시 코드와 함께 설명해 드리겠습니다. 이 코드는 RNN을 사용하여 가상의 시퀀스 데이터를 처리하고, 예측을 수행하는 간단한 예제입니다.
1. 필요한 라이브러리 임포트
먼저, PyTorch와 관련 라이브러리를 임포트합니다. RNN 모듈과 기본적인 데이터 처리를 위한 도구들을 포함시키겠습니다.
import torch
import torch.nn as nn
import torch.optim as optim
2. RNN 모델 정의
RNN 클래스를 정의합니다. 이 클래스는 nn.Module을 상속받아, RNN 레이어를 포함하고 있습니다. 여기서는 하나의 RNN 레이어만 사용하지만, 필요에 따라 레이어 수를 증가시킬 수 있습니다.
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
# RNN Layer
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
# Fully connected layer
self.linear = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 초기 hidden state
h0 = torch.zeros(1, x.size(0), self.hidden_size)
# RNN의 출력과 최종 hidden state
out, hn = self.rnn(x, h0)
# 마지막 타임 스텝의 출력을 가져옴
out = out[:, -1, :]
out = self.linear(out)
return out
3. 모델 인스턴스 생성 및 설정
모델을 초기화하고, 손실 함수와 최적화 방법을 설정합니다.
# 모델 파라미터 설정
input_size = 10 # 입력 크기
hidden_size = 20 # 은닉 상태의 크기
output_size = 1 # 출력 크기
model = SimpleRNN(input_size, hidden_size, output_size)
# 손실 함수 및 최적화 방법
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
4. 학습 데이터 준비
간단한 가상 데이터를 준비하고, 학습을 수행합니다.
# 임의의 입력 데이터와 타깃 데이터
x_train = torch.randn(100, 5, input_size) # (batch_size, sequence_length, input_size)
y_train = torch.randn(100, output_size) # (batch_size, output_size)
# 학습 과정
for epoch in range(100):
model.train()
optimizer.zero_grad()
output = model(x_train)
loss = criterion(output, y_train)
loss.backward()
optimizer.step()
if (epoch+1) % 10 == 0:
print(f'Epoch {epoch+1}, Loss: {loss.item()}')
이 코드는 PyTorch를 사용하여 간단한 RNN 모델을 구현하고, 임의의 데이터에 대해 학습을 수행하는 방법을 보여줍니다. RNN은 다양한 시퀀스 데이터 처리 작업에 확장하여 사용할 수 있으며, 실제 응용 예제에 맞게 입력 크기, 출력 크기, 레이어 수 등을 조정할 수 있습니다.
RNN (Recurrent Neural Network)은 주로 시간적으로 연속된 데이터나 시퀀스 데이터를 처리하는 데 적합한 신경망 아키텍처입니다. CNN과 RNN은 각각 공간적 및 시간적 데이터를 다루는 데 특화된 구조적 차이를 가지고 있습니다. 다음은 CNN과 RNN의 주요 차이점을 요약한 내용입니다.
CNN과 RNN의 구조적 차이
- 핵심 아키텍처:
- CNN: CNN은 주로 이미지와 같은 고차원 데이터에서 공간적 계층 구조를 탐지하는 데 사용됩니다. 이를 위해 convolutional layer, pooling layer, 그리고 필요에 따라 fully connected layer를 포함합니다.
- RNN: RNN은 데이터가 시간에 따라 연속적으로 발생하고 각 시점의 데이터가 이전 시점의 데이터에 의존하는 시퀀스 데이터에 적합합니다. RNN은 과거 정보를 현재의 결정에 반영할 수 있는 순환 연결 구조를 가집니다.
- 파라미터 공유:
- CNN: CNN은 공간적으로 파라미터를 공유하여 이미지 전체에서 유사한 특징을 효과적으로 학습합니다.
- RNN: RNN은 시간적으로 파라미터를 공유하며, 각 시퀀스에서 발생하는 패턴을 반복적으로 학습할 수 있습니다. 즉, 한 시점의 학습이 다음 시점의 학습에 영향을 미칩니다.
- 메모리 유지:
- CNN: CNN은 입력 데이터의 공간적 구조만을 고려하며, 입력 간의 시간적 관계를 기억하지 못합니다.
- RNN: RNN은 이전 시점의 출력(또는 상태)를 다음 시점의 입력으로 사용하여, 시간에 걸쳐 정보를 "기억"할 수 있습니다. 이러한 특성은 시퀀스 데이터 처리에 필수적입니다.
- 적용 분야:
- CNN: 이미지 분류, 객체 탐지, 이미지 세그멘테이션 등 이미지 데이터 처리에 주로 사용됩니다.
- RNN: 자연어 처리(NLP), 음성 인식, 시계열 데이터 분석 등 시간적 연속성을 요구하는 데이터를 다루는 데 사용됩니다.
결론
CNN과 RNN은 각각 공간적 특징과 시간적 특징을 강조하는 데이터를 다루는 데 최적화되어 있습니다. CNN은 이미지 같은 데이터에서 복잡한 패턴을 효과적으로 추출할 수 있지만, 시간적 순서를 다루는 것에는 한계가 있습니다. 반면, RNN은 시간에 따른 데이터의 연속성을 잘 처리할 수 있으나, 긴 시퀀스 데이터에서는 정보를 장기간 유지하는 데 어려움이 있을 수 있습니다(이를 해결하기 위해 LSTM이나 GRU 같은 고급 RNN 구조가 개발되었습니다). 따라서 선택하는 아키텍처는 항상 처리하려는 데이터의 특성과 작업의 요구에 따라 결정되어야 합니다.
'DeepLearning' 카테고리의 다른 글
FC layer의 단점을 극복한 global average pooling, GAP (0) | 2024.07.25 |
---|---|
딥러닝 물고기 책, 밑바닥부터 시작하는 딥러닝 (0) | 2024.07.24 |
ReLU (Rectified Linear Unit) (0) | 2024.06.27 |
인공신경망은 MLE 기계다! (0) | 2024.06.27 |
ML,DL에서는 Convex 함수가 무조건 좋은 함수다? (0) | 2024.06.27 |
- Total
- Today
- Yesterday
- 오블완
- #패스트캠퍼스 #패스트캠퍼스ai부트캠프 #업스테이지패스트캠퍼스 #upstageailab#국비지원 #패스트캠퍼스업스테이지에이아이랩#패스트캠퍼스업스테이지부트캠프
- t5
- LIST
- Array
- 리스트
- nlp
- Python
- recursion #재귀 #자료구조 # 알고리즘
- PEFT
- cnn
- Numpy
- LLM
- #패스트캠퍼스 #패스트캠퍼스AI부트캠프 #업스테이지패스트캠퍼스 #UpstageAILab#국비지원 #패스트캠퍼스업스테이지에이아이랩#패스트캠퍼스업스테이지부트캠프
- Lora
- Hugging Face
- 손실함수
- 티스토리챌린지
- English
- #패스트캠퍼스 #UpstageAILab #Upstage #부트캠프 #AI #데이터분석 #데이터사이언스 #무료교육 #국비지원 #국비지원취업 #데이터분석취업 등
- git
- RAG
- speaking
- 해시
- classification
- Github
- 코딩테스트
- Transformer
- clustering
- 파이썬
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |