질문
이름과 질문을 적어주세요
유성현
ResNet 이후 더 발전된 모델은 어떤 것들이 있을지 궁금합니다.
https://deep-learning-study.tistory.com/528
https://benlee73.tistory.com/33
박소정
6.1.4. 패딩을 하면 데이터의 가장자리 정보들이 사라지는 문제를 해결할 수 있는데 책에는 풀링 연산을 할때 보통 패딩을 추가하지 않는 것 같습니다. 연산시간이 너무 길어져 패딩을 하지 않는 것일까요?
조민진
GoogLeNet에서 특징을 효율적으로 추출하기 위해 1×1, 3×3, 5×5의 합성곱 연산을 각각 수행한다고 하였는데 한꺼번에 학습하는 것보다 효과가 더 높은 이유는 무엇일까요?
6장. 합성곱 신경망II
6.1 이미지 분류를 위한 신경망
입력 데이터로 이미지를 사용한 분류(classification)는
특정 대상이 영상 내에 존재하는지 여부를 판단
6.1.1 LeNet-5
•
최초로 얀 르쿤(Yann LeCun)이 개발
•
현재 CNN의 초석
•
합성곱(convolutional)과 다운 샘플링(sub-sampling)(혹은 풀링)을 반복적으로 거치면서
마지막에 완전연결층에서 분류를 수행
•
LeNet-5를 사용하는 예제(앞 장에서 사용한 개와 고양이 데이터셋을 다시 사용)
32×32 크기의 이미지에 합성곱층과 최대 풀링층이 쌍으로 두 번 적용된 후 완전연결층을 거쳐
이미지가 분류되는 신경망
•
LeNet의 아키텍처로 개와 고양이 구분하는 모델 만들기
필요한 라이브러리 호출, 모델 학습에 필요한 데이터셋의 전처리(텐서 변환)
# 코드 6-1 필요한 라이브러리 호출
import torch
import torchvision
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms # ------ 이미지 변환(전처리) 기능을 제공하는 라이브러리
from torch.autograd import Variable
from torch import optim # ------ 경사 하강법을 이용하여 가중치를 구하기 위한 옵티마이저 라이브러리
import torch.nn as nn
import torch.nn.functional as F
import os # ------ 파일 경로에 대한 함수들을 제공
import cv2 # 아나콘다 prompt에서 pip install opencv-python
from PIL import Image
from tqdm import tqdm_notebook as tqdm # ------ 진행 상황을 가시적으로 표현해 주는데, 특히 모델의 학습 경과를 확인하고 싶을 때 사용하는 라이브러리
import random
from matplotlib import pyplot as plt
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # ------ 파이토치는 텐서플로와 다르게 GPU를 자동으로 할당해 주지 않기 때문에 GPU 할당을 모델과 데이터에 선언해 주어야 합니다. 단 이 장에서는 CPU를 사용합니다.
# cuda : gpu
# 코드 6-2 이미지 데이터셋 전처리
class ImageTransform():
def __init__(self, resize, mean, std):
self.data_transform = {
'train': transforms.Compose([
transforms.RandomResizedCrop(resize, scale=(0.5,1.0)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean, std)
]), # 토치 비전 라비브러리(이미지에 대한 전처리 쉽게 해준다)
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(resize),
transforms.ToTensor(),
transforms.Normalize(mean, std)
])
}
def __call__(self, img, phase): # __call__메서드는 인스턴스가 호출되었을 때 실행
return self.data_transform[phase](img)
Python
복사
import cv2 → 아나콘다 prompt에서 pip install opencv-python
이미지가 위치한 디렉터리에서 데이터를 불러온 후 훈련용 400개, 검증용92개, 테스트 10개 분리
# 코드 6-3 이미지 데이터셋을 불러온 후 훈련, 검증, 데스트로 분리
cat_directory = 'C:/big_py/dogs-vs-cats/Cat/' # 파일명 한글 들어가면 cv2.imread에서 error
dog_directory = 'C:/big_py/dogs-vs-cats/Dog/'
cat_images_filepaths = sorted([os.path.join(cat_directory, f) for f in
os.listdir(cat_directory)]) # ------ ①
dog_images_filepaths = sorted([os.path.join(dog_directory, f) for f in
os.listdir(dog_directory)])
images_filepaths = [*cat_images_filepaths, *dog_images_filepaths] # ------ 개와 고양이 이미지들을 합쳐서 images_filepaths에 저장
correct_images_filepaths = [i for i in images_filepaths if cv2.imread(i) is not None] # ------ ②
random.seed(42) # 난수 생성
random.shuffle(correct_images_filepaths)
train_images_filepaths = correct_images_filepaths[:400] # ------ 훈련용 400개의 이미지
val_images_filepaths = correct_images_filepaths[400:-10] # ------ 검증용 92개의 이미지
test_images_filepaths = correct_images_filepaths[-10:] # ------ 테스트용 열 개의 이미지
print(len(train_images_filepaths), len(val_images_filepaths), len(test_images_filepaths))
# 코드 6-4 테스트 데이터셋 이미지 확인 함수
def display_image_grid(images_filepaths, predicted_labels=(), cols=5):
rows = len(images_filepaths) // cols
figure, ax = plt.subplots(nrows=rows, ncols=cols, figsize=(12, 6))
for i, image_filepath in enumerate(images_filepaths):
image = cv2.imread(image_filepath)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # ------ ①
true_label = os.path.normpath(image_filepath).split(os.sep)[-2] # ------ ②
predicted_label = predicted_labels[i] if predicted_labels else true_label # ------ ③
color = "green" if true_label == predicted_label else "red" # ------ 예측과 정답(레이블)이 동일하면 초록색으로 표시하고, 그렇지 않다면 빨간색으로 표시
ax.ravel()[i].imshow(image) # ------ 개별 이미지를 출력
ax.ravel()[i].set_title(predicted_label, color=color) # ------ predicted_label을 타이틀로 사용
ax.ravel()[i].set_axis_off() # ------ 이미지의 축 제거
plt.tight_layout() # ------ 이미지의 여백을 조정
plt.show()
# 코드 6-5 테스트 데이터셋 이미지를 출력
display_image_grid(test_images_filepaths)
Python
복사
cv2.imread(i) → cat_directory, dog_directory에 파일명 한글들어가면 error
최종 목적 고양이와 개가 포함될 확률을 코드로 구현하기 위해
고양이가 있는 이미지의 레이블은 0, 개가 있는 이미지의 레이블은 1이 되도록 하는 코드
(라벨링)
# 코드 6-6 이미지 데이터셋 클래스 정의
# 레이블(정답) 이미지에서 고양이와 개가 포함될 확률을 코드로 구현, 고양이가 있는 이미지의 레이블은 0, 개가 있는 이미지의 레이블은 1
class DogvsCatDataset(Dataset):
def __init__(self, file_list, transform=None, phase='train'): # ------데이터셋의 전처리(데이터 변형 적용)
self.file_list = file_list
self.transform = transform # ------ DogvsCatDataset 클래스를 호출할 때 transform에 대한 매개변수를 받아 옵니다.
self.phase = phase # ------ ‘train’ 적용
def __len__(self): # ------ images_filepaths 데이터셋의 전체 길이를 반환
return len(self.file_list)
def __getitem__(self, idx): # ------ 데이터셋에서 데이터를 가져오는 부분으로 결과는 텐서 형태가 됩니다.
img_path = self.file_list[idx]
img = Image.open(img_path) # ------ img_path 위치에서 이미지 데이터들을 가져옵니다.
img_transformed = self.transform(img, self.phase) # ------ 이미지에 ‘train’ 전처리를 적용
label = img_path.split('/')[-1].split('.')[0] # 이미지 파일명 dog, cat 구분
if label == 'dog':
label = 1
elif label == 'cat':
label = 0
return img_transformed, label
# 코드 6-7 변수 값 정의
# 전처리에서 사용할 변수 정의
size = 224
mean = (0.485, 0.456, 0.406)
std = (0.229, 0.224, 0.225)
batch_size = 32
# 코드 6-8 이미지 데이터셋 정의
# 훈련 데이터의 크기와 레이블에 대한 출력 결과
train_dataset = DogvsCatDataset(train_images_filepaths, transform=ImageTransform(size, mean, std), phase='train') # ------ 훈련 이미지에 train_transforms를 적용
val_dataset = DogvsCatDataset(val_images_filepaths, transform=ImageTransform(size, mean, std), phase='val') # ------ 검증 이미지에 test_transforms를 적용
index = 0
print(train_dataset.__getitem__(index)[0].size()) # ------ 훈련 데이터(train_dataset.__getitem__[0][0])의 크기(size()) 출력
print(train_dataset.__getitem__(index)[1]) # ------ 훈련 데이터의 레이블 출력
# 1은 개, 0은 고양이
Python
복사
훈련 데이터의 크기와 레이블 출력
0 → index 0번째 훈련데이터 레이블이 ‘고양이’
데이터를 메모리 효율을 위해 테이터로더에서 배치 크기만큼 분할하여 불러오기
데이터셋을 학습시킬 모델의 네트워크 설계(클래스 생성)
# 코드 6-9 데이터로더의 정의
# 데이터 로더로 배치 관리 ->한 번에 모든 데이터를 불러오면 메모리에 부담, 데이터를 그룹으로 쪼개서 조금씩 불러오기
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # ------ ①
val_dataloader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
dataloader_dict = {'train': train_dataloader, 'val': val_dataloader} # ------ 훈련 데이터셋(train_dataloader)과 검증 데이터셋(val_dataloader)을 합쳐서 표현
# 데이터 로더를 이용하여 훈련 데이터셋을 메모리로 불러온 후 데이터셋의 크기와 레이블 출력
batch_iterator = iter(train_dataloader)
inputs, label = next(batch_iterator)
print(inputs.size())
print(label)
# 코드 6-10 모델의 네트워크 클래스
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.cnn1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1, padding=0) # ------ 2D 합성곱층이 적용됩니다. 이때 입력 형태는 (3, 224, 224)가 되며 출력 형태는 (weight-kernel_size+1)/stride에 따라 (16, 220, 220)이 됩니다.
self.relu1 = nn.ReLU() # ------ ReLU 활성화 함수입니다.
self.maxpool1 = nn.MaxPool2d(kernel_size=2) # ------ 최대 풀링이 적용됩니다. 적용 이후 출력 형태는 220/2가 되어 (16, 110, 110)입니다.
self.cnn2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=0) #------ 또다시 2D 합성곱층이 적용되며 출력 형태는 (32, 106, 106)입니다.
self.relu2 = nn.ReLU()
self.maxpool2 = nn.MaxPool2d(kernel_size=2) # ------ 최대 풀링이 적용되며 출력 형태는 (32, 53, 53)입니다.
self.fc1 = nn.Linear(32*53*53, 512)
self.relu5 = nn.ReLU()
self.fc2 = nn.Linear(512, 2)
self.output = nn.Softmax(dim=1)
def forward(self, x):
out = self.cnn1(x)
out = self.relu1(out)
out = self.maxpool1(out)
out = self.cnn2(out)
out = self.relu2(out)
out = self.maxpool2(out)
out = out.view(out.size(0), -1) # ------ 완전연결층에 데이터를 전달하기 위해 데이터 형태를 1차원으로 바꿉니다.
out = self.fc1(out)
out = self.fc2(out)
out = self.output(out)
return out
# 코드 6-11 모델 객체 생성
model = LeNet() # Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same
model.to("cuda")
print(model)
# 코드 6-12 torchsummary 라이브러리를 이용한 모델의 네트워크 구조 확인
from torchsummary import summary
summary(model, input_size=(3, 224, 224))
# 코드 6-13 학습 가능한 파라미터 수 확인
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'The model has {count_parameters(model):,} trainable parameters')
Python
복사
input type과 weight type이 동시에 cuda이어야 하는데 그게 아니라서 그렇다.
다시 말해 input type(x,y)는 cuda를 먹였는데 weight type(신경망)은 cuda를 먹이지 않아서 생기는 에러 → model.to("cuda")
총 파라미터 수, 입력 크기, 네트워크의 총크기
옵티마이저와 손실 함수 정의하고 모델 학습 → 학습 용도이기 때문에 model.train()사용
# 코드 6-14 옵티마이저와 손실 함수 정의
# 경사 하강법으로 모멘텀 SGD사용
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9) # ------ ①
criterion = nn.CrossEntropyLoss()
# 코드 6-15 모델의 파라미터와 손실 함수를 GPU에 할당
model = model.to(device) # device -> "cuda"
criterion = criterion.to(device)
# 코드 6-16 모델 학습 함수 정의
# model.train()사용
def train_model(model, dataloader_dict, criterion, optimizer, num_epoch):
since = time.time()
best_acc = 0.0
for epoch in range(num_epoch): # ------ epoch를 10으로 설정했으므로 10회 반복
print('Epoch {}/{}'.format(epoch+1, num_epoch))
print('-'*20)
for phase in ['train', 'val']:
if phase == 'train':
model.train() # ------ 모델을 학습시키겠다는 의미
else:
model.eval()
epoch_loss = 0.0
epoch_corrects = 0
for inputs, labels in tqdm(dataloader_dict[phase]): # ------ 여기에서 dataloader_dict는 훈련 데이터셋(train_loader)을 의미
inputs = inputs.to(device) # ------ 훈련 데이터셋을 CPU에 할당
labels = labels.to(device)
optimizer.zero_grad() # ------ 역전파 단계를 실행하기 전에 기울기(gradient)를 0으로 초기화
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels) # ------ 손실 함수를 이용한 오차 계산
if phase == 'train':
loss.backward() # ------ 모델의 학습 가능한 모든 파라미터에 대해 기울기를 계산
optimizer.step() # ------ optimizer의 step 함수를 호출하면 파라미터를 갱신
epoch_loss += loss.item() * inputs.size(0) # ------ ①
epoch_corrects += torch.sum(preds == labels.data) # ------ 정답과 예측이 일치하면 그것의 합계를 epoch_corrects에 저장
epoch_loss = epoch_loss / len(dataloader_dict[phase].dataset) # ------ 최종 오차 계산(오차를 데이터셋의 길이(개수)로 나누어서 계산)
epoch_acc = epoch_corrects.double() / len(dataloader_dict[phase].dataset) # ------ 최종 정확도(epoch_corrects를 데이터셋의 길이(개수)로 나누어서 계산)
print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
if phase == 'val' and epoch_acc > best_acc: # ------ 검증 데이터셋에 대한 가장 최적의 정확도를 저장
best_acc = epoch_acc
best_model_wts = model.state_dict()
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_acc))
return model
# 코드 6-17 모델 학습
import time
num_epoch = 10
model = train_model(model, dataloader_dict, criterion, optimizer, num_epoch)
# conda install -c conda-forge ipywidgets 설치
Python
복사
train_model → 아나콘다 prompt에서 conda install -c conda-forge ipywidgets 설치
검증 테이터셋 이용 최고 52.%정확도 별로 안좋다 → 데이터셋 늘리면 좋아진다.
테스트 데이터셋을 모델에 적용하여 정확도 측정 → model.eval() 사용
# 코드 6-18 모델 테스트를 위한 함수 정의
import pandas as pd
id_list = []
pred_list = []
_id = 0
with torch.no_grad(): # ------ 역전파 중 텐서들에 대한 변화도를 계산할 필요가 없음을 나타내는 것으로, 훈련 데이터셋의 모델 학습과 가장 큰 차이점입니다.
for test_path in tqdm(test_images_filepaths): # ------ 테스트 데이터셋 이용
img = Image.open(test_path)
_id = test_path.split('/')[-1].split('.')[1]
transform = ImageTransform(size, mean, std)
img = transform(img, phase='val') # ------ 테스트 데이터셋 전처리 적용
img = img.unsqueeze(0) # ------ ①
img = img.to(device)
model.eval()
outputs = model(img)
preds = F.softmax(outputs, dim=1)[:, 1].tolist() # 0: 고양이일 확률, 1: 개일 확률
id_list.append(_id)
pred_list.append(preds[0])
res = pd.DataFrame({
'id': id_list,
'label': pred_list
}) # ------ 테스트 데이터셋의 예측 결과인 id와 레이블(label)을 데이터 프레임에 저장
res.sort_values(by='id', inplace=True)
res.reset_index(drop=True, inplace=True)
res.to_csv('C:/big_py/LeNet', index=False) # ------ 데이터 프레임을 CSV 파일로 저장
# 예측 결과 레이블이 0.5보다 크면 개, 작으면 고양이를 의미
# 코드 6-19 테스트 데이터셋의 예측 결과 호출
res.head(10)
# 코드 6-20 테스트 데이터셋 이미지를 출력하기 위한 함수 정의
class_ = classes = {0:'cat', 1:'dog'} # ------ 개와 고양이에 대한 클래스 정의
def display_image_grid(images_filepaths, predicted_labels=(), cols=5):
rows = len(images_filepaths) // cols
figure, ax = plt.subplots(nrows=rows, ncols=cols, figsize=(12, 6))
for i, image_filepath in enumerate(images_filepaths):
image = cv2.imread(image_filepath)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
a = random.choice(res['id'].values) # ------ 데이터 프레임의 id라는 칼럼에서 임의로 데이터를 가져옵니다.
label = res.loc[res['id'] == a, 'label'].values[0]
if label > 0.5: # ------ 레이블 값이 0.5보다 크다면 개
label = 1
else: # ------ 레이블 값이 0.5보다 작다면 고양이 (고양이로 하는 이유)
label = 0 # 개일 확률 0.4 -> 고양이 확률 0.6 ( 개 + 고양이 = 1 )
ax.ravel()[i].imshow(image)
ax.ravel()[i].set_title(class_[label])
ax.ravel()[i].set_axis_off()
plt.tight_layout()
plt.show()
# 코드 6-21 테스트 데이터셋 예측 결과 이미지 출력
display_image_grid(test_images_filepaths)
Python
복사
label이 소프트맥스 결과값
(개일 확률)
레이블이 0.5보다 크면 개
0.5보다 작으면 고양이
개 확률 + 고양이 확률 = 1
6.1.2 AlexNet
6.1.3 VGGNet
테스트 데이터셋을 이용해 모델의 예측 결과 알아보기
# 코드 6-59 테스트 데이터셋을 이용한 모델 성능 측정
model.load_state_dict(torch.load('C:/big_py/VGG-model.pt')) # 저장된 모델 로드
test_loss, test_acc = evaluate(model, test_iterator, criterion, device)
print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
Python
복사
테스트 데이터셋 예측 결과 시각화
# 코드 6-60 테스트 데이터셋을 이용한 모델의 예측 확인 함수
def get_predictions(model, iterator):
model.eval()
images = []
labels = []
probs = []
with torch.no_grad():
for (x, y) in iterator:
x = x.to(device)
y_pred, _ = model(x)
y_prob = F.softmax(y_pred, dim=-1)
top_pred = y_prob.argmax(1, keepdim=True) # ------ ①
images.append(x.cpu())
labels.append(y.cpu())
probs.append(y_prob.cpu())
images = torch.cat(images, dim=0) # ------ ②
labels = torch.cat(labels, dim=0)
probs = torch.cat(probs, dim=0)
return images, labels, probs
# 코드 6-61 예측 중에서 정확하게 예측한 것을 추출
images, labels, probs = get_predictions(model, test_iterator)
pred_labels = torch.argmax(probs, 1) # ------ ①
corrects = torch.eq(labels, pred_labels) # ------ 예측과 정답이 같은지 비교
correct_examples = []
for image, label, prob, correct in zip(images, labels, probs, corrects): # ------ ②
if correct:
correct_examples.append((image, label, prob))
correct_examples.sort(reverse=True, key=lambda x: torch.max(x[2], dim=0).values) # ------ ③
# 코드 6-62 이미지 출력을 위한 전처리
def normalize_image(image):
image_min = image.min()
image_max = image.max()
image.clamp_(min=image_min, max=image_max) # ------ torch.clamp는 주어진 최소(min), 최대(max)의 범주에 이미지가 위치하도록 합니다.
image.add_(-image_min).div_(image_max-image_min+1e-5) # ------ ①
return image
# 코드 6-63 모델이 정확하게 예측한 이미지 출력 함수
def plot_most_correct(correct, classes, n_images, normalize=True):
rows = int(np.sqrt(n_images)) # ------ np.sqrt는 제곱근을 계산(0.5를 거듭제곱)
cols = int(np.sqrt(n_images))
fig = plt.figure(figsize=(25,20))
for i in range(rows*cols):
ax = fig.add_subplot(rows, cols, i+1) # ------ 출력하려는 그래프 개수만큼 subplot을 만듭니다.
image, true_label, probs = correct[i]
image = image.permute(1, 2, 0) # ------ ①
true_prob = probs[true_label]
correct_prob, correct_label = torch.max(probs, dim=0)
true_class = classes[true_label]
correct_class = classes[correct_label]
if normalize: # ------ 본래 이미지대로 출력하기 위해 normalize_image 함수 호출
image = normalize_image(image)
ax.imshow(image.cpu().numpy())
ax.set_title(f'true label: {true_class} ({true_prob:.3f})\n' \
f'pred label: {correct_class} ({correct_prob:.3f})')
ax.axis('off')
fig.subplots_adjust(hspace=0.4)
# 코드 6-64 예측 결과 이미지 출력
classes = test_dataset.classes
N_IMAGES = 5
plot_most_correct(correct_examples, classes, N_IMAGES)
Python
복사
6.1.4 GoogLeNet
6.1.5 ResNet
질문
유성현
ResNet 이후 더 발전된 모델은 어떤 것들이 있을지 궁금합니다.
https://deep-learning-study.tistory.com/528
https://benlee73.tistory.com/33
•
DenseNet
resnet의 숏컷을 모든 레이어로 확장하게 끔 바꾼것
resnet보다 기울기 소실 문제를 완화하고 파라미터 수와 연산량이 적다는 장점이 있다
•
ResNext
같은 block을 반복적으로 구축해서 더 적은 파라미터로 이미지 분류
ResNext 는 다른 모델들 (ResNet-101/152, ResNet200, Inception-v3, Inception-ResNet-v2) 보다 더 간단한 구조를 가졌지만 더 나은 성능을 가진다.
박소정
6.1.4. 패딩을 하면 데이터의 가장자리 정보들이 사라지는 문제를 해결할 수 있는데 책에는 풀링 연산을 할때 보통 패딩을 추가하지 않는 것 같습니다. 연산시간이 너무 길어져 패딩을 하지 않는 것일까요?
입출력 크기 맞추기 위한 것을 제외하고
풀링 연산에 패딩하는 것은 의미 없음,
3×3 최대 풀링은 입력과 출력의 높이와 너비가 같아야하므로, 패딩 연산 추가
풀링 → 특성 맵의 차원을 다운(연산량 감소), 주요한 특성 벡터 추출이 목적
최대풀링, 평균풀링 패딩(제로)해도 의미가 없음
조민진
GoogLeNet에서 특징을 효율적으로 추출하기 위해 1×1, 3×3, 5×5의 합성곱 연산을 각각 수행한다고 하였는데 한꺼번에 학습하는 것보다 효과가 더 높은 이유는 무엇일까요?
한꺼번에 학습?
1*1은 특성맵(채널)의 갯수를 줄이는 목적으로 사용
5*5*3 (rgb) → 1*1, 1*1, 1*1 → 5*5*1 (5*5는 유지 ,채널만 줄인다)
3*3 필터로 추출한 특성, 5*5 필터로 추출한 특성 쌓아서 같이 쓴다.