열심히 코딩 하숭!

9. CNN을 활용한 이미지 인식 | 파이토치 딥러닝 프로그래밍 본문

프로그래밍 책/파이토치 딥러닝 프로그래밍

9. CNN을 활용한 이미지 인식 | 파이토치 딥러닝 프로그래밍

채숭이 2024. 4. 21. 00:28

 

09 CNN을 활용한 이미지 인식

9.1 문제 정의

CIFAR-10

  • 32 x 32 화소의 컬러 이미지 데이터
  • 이미지를 학습하여 해당하는 이미지의 카테고리를 예측
  • 컬러이므로 → (3, 32, 32)

🌟채널🌟

  • 이미지 → ‘색, 가로, 세로’의 3계 텐서의 구조를 유지 (여기에서 ‘색’이 채널!)
    • RGB라면 채널은 3
    • 흑백 등 단일 색상일 경우 채널은 1
  • 두번째 층 이후에서는 ‘색’에 해당하는 인덱스는 더 이상 색의 의미를 갖지 않게 된다
    • ⇒ 학습하면서 다른 특징에 초점을 맞출 수 있기 때문
  • 이렇게 깊이에 해당하는 인덱스는 ‘채널’이라고 불림
📌 채널이 4개 이상일 경우? 
→ 다중 모달 데이터 (자연어처리)
→ 시간 차원 (비디오)
→ 3D 이미지
→ 특징 추출: 일부 심층 학습 아키텍처에서는 채널 수를 증가시켜 모델의 표현력을 향상시키기 위해 사용될 수 있음.
(이는 특정 계층에서 다양한 특징을 추출하고 다양한 측면에서 입력 데이터를 보다 효과적으로 모델링하기 위해 사용될 수 있다.)

채널을 색으로 예를들어 소개하는 블로그가 많아서 헷갈렸음! 색도 하나의 “특징”으로 생각하면 이해됨

 

 

9.3 CNN의 처리 개요

합성곱(Convolution)

  • 커널: 정방형의 배열

  • 처리 방법: 원본 이미지를 커널과 같은 크기의 정방형 영역으로 잘라내고 대응하는 각 요소에 곱연산을 취함
  • 도형의 특징량을 추출하는데 탁월함

  • 커널은 입력 채널의 분량만큼 있어야 한다
  • 학습 대상: 커널 배열이 신경망에서의 ‘파라미터’에 해당
  • 커널의 특징

📌 커널이 위치를 이동하며 학습이 이뤄지므로 위치의 이동과 관련이 없는 특징량을 검출할 수 있게 된다
  • CNN은 이미지 내의 다양한 특징을 위치에 무관하게 검출할 수 있다.
  • 커널이 이리저리 움직이며 특징을 검출해내기 때문!!!
    (위치가 바뀌더라도 특징이 그대로면 발견할 수 있다)

 

 

풀링(Pooling)

  • 입력 데이터의 다운샘플링을 통해 공간 크기를 줄이고 중요한 특징을 강조하는 과정
  • maxpooling: 2x2와 같이 작은 사각형으로 이미지를 잘라내어 그 범위 안에서 최대값을 구함
  • 풀링은 보통 컨볼루션과 달리 중첩되는 영역이 없게끔 옮겨가며 처리하는 게 일반적이다
  • 이미지 축소와 동일한 효과를 얻을 수 있음
 
📌 컨볼루션 / 풀링에서 감소되는 이미지 크기 계산 방법

  • 컨볼루션: 이미지 가로 크기 - 2 * round down(kernel 가로 크기 / 2)
  • 풀링(중첩되는 영역이 없을 경우): round down(이미지 가로 크기 / kernel 가로 크기)

📍 Padding
  • 일반적인 컨볼루션을 진행하면 이미지 크기가 감소함
  • 입력 이미지 가장자리에 가상의 숫자를 덧붙여서 출력 이미지의 크기를 조절
  • zero padding: 0으로 채움
    0 0 1 3 4 1 0 0
  • replicate padding: 가장자리 값으로 채움
    1 1 1 3 4 1 1 1
  • mirror padding: 대칭으로 채움
    3 1 1 3 4 1 1 4
- 이미지 크기를 유지하기 위한 패딩 사이즈: round down(kernel size / 2)



 

 

9.4 파이토치에서 CNN을 구현하는 방법

nn.Conv2d

  • 합성곱 함수
  • 파라미터 가짐

nn.MaxPool2d

  • maxpool 풀링 함수
  • 파라미터 X

nn.Conv2d와 nn.MaxPool2d 코드

conv1 = nn.Conv2d(3, 32, 3) # 입력 채널 수 / 출력 채널 수 / 커널 사이즈(nxn)
relu = nn.ReLU(inplace=True)
conv2 = nn.Conv2d(32, 32, 3)
maxpool = nn.MaxPool2d((2,2)) # 사각형 가로, 세로

print(conv1)
print(conv1.weight.shape)
print(conv1.bias.shape)
print(conv2.weight.shape)
print(conv2.bias.shape)

## 결과 ##
Conv2d(3, 32, kernel_size=(3,3), stride=(1,1))
torch.Size([32, 3, 3, 3]) # weight는 4계 텐서임
torch.Size([32])
torch.Size([32, 32, 3, 3]) # weight는 4계 텐서임
torch.Size([32])
  • 여기는 컨볼루션 속의 weight와 bias를 shape으로 나타낸 것임
📍 컨볼루션에서 가중치 함수의 shape에 대해

[32, 3, 3, 3]

  1) 32개의 출력 채널수를 가져야 한다!


  2) 입력 이미지가 RGB 채널을 가지니까 입력 채널 수인 3!!

  3, 4) 3x3 짜리의 커널이!




⇒ 학교 강의자료

  • 여기에서는 필터와 채널을 명확하게 구분하여 설명
  • 필터의 개수가 출력하고 싶은 채널수의 개수와 같아야 함
  • 3 x 3 x 3 에서 3은 입력 채널 수 / 3 x 3은 커널 크기
  • 그러므로 32 x 3 x 3 x 3이 나오게 되는 것!

 

 

컨볼루션, 풀링 시뮬레이션 코드

# 임의로 텐서 생성
inputs = torch.randn(100, 3, 32, 32)
print(inputs.shape)

# CNN 전반부 처리
x1 = conv1(inputs)
x2 = relu(x1)
x3 = conv2(x2)
x4 = relu(x3)
x5 = maxpool(x4)
print(inputs.shape, x1.shape, x2.shape, x3.shape, x4.shape, x5.shape)

## 결과 ##
# [데이터 건수, 채널 수, 가로 화소 수, 세로 화소 수]
torch.Size([100, 3, 32, 32])
torch.Size([100, 32, 30, 30]) -> x1
torch.Size([100, 32, 30, 30]) -> x2
torch.Size([100, 32, 28, 28]) -> x3
torch.Size([100, 32, 28, 28]) -> x4
torch.Size([100, 32, 14, 14]) -> x5
  • 여기는 데이터가 함수에 들어갔다가 나왔을 때의 shape를 나타낸 것
  • 첫번째 숫자[학습셋 데이터 건수]: 일정하게 100
  • 두번째 숫자[채널 수]: 처음에는 RGB 채널이 3장이므로 3이 입력되었고 이후 출력 채널수를 = 32로 설정함에 따라, 두번째값이 32로 나타났다
  • 세번째, 네번째 숫자[가로, 세로 화소 수]: 처음에는 이미지 사이즈인 32x32이고, 이후 컨볼루션 그리고 풀링으로 인해 화소 수가 감소하는 것을 확인할 수 있다

nn.Sequential

  • 여러 함수의 합성 함수로 구성되어 직렬로 이어져 있음
  • 파이토치에서 ‘컨테이너’라고 불림
features = nn.Sequential(
	conv1,
	relu,
	conv2,
	relu,
	maxpool
)
outputs = features(inputs)

nn.Flatten

  • 1계화 함수
  • 선형 함수(nn.Linear)에서 사용할 수 있게끔, 가로 1열의 1계 텐서로 만들어줌 (마지막 출력에 대한 조작이 필요할 때 사용)
flatten = nn.Flatten()
outputs2 = flatten(outputs)
print(outputs.shape)
print(outputs2.shape)

## 결과 ##
torch.Size([100, 32, 14, 14])
torch.Size([100, 6272]) # 32 x 14 x 14

 

9.5 공통 함수 사용하기

eval_loss (손실 계산)

  • 손실을 계산하면, 변수를 사용하여 손실 계산 그래프를 시각화할 수 있다
  • 흐름
    • loader: 데이터 로더로 입력 데이터와 정답 데이터를 가져온다
    • 입력 데이터와 모델 인스턴스로부터 예측 값을 계산
    • 예측 값과 정답 데이터를 사용하여 손실 계산
def eval_loss(loader, device, net, criterion):
	for images, labels in loader: # 처음 한 개 세트를 가져옴
		break
	
	# 디바이스 할당
	inputs = images.to(device)
	labels = labels.to(device)
	
	# 예측 계산
	outputs = net(inputs)
	
	# 손실 계산
	loss = criterion(outputs, labels)
	
	return loss

fit (학습)

  • 반복 계산 부분을 한 번에 처리
  • 인수
    • net: 학습 대상의 모델 인스턴스
    • optimizer: 최적화 함수의 인스턴스
    • criterion: 손실 함수의 인스턴스
    • num_epochs: 반복 횟수
    • train_loader: 훈련용 데이터로더
    • test_loader: 검증용 데이터로더
    • device: GPU 또는 CPU
    • history: 계산 도중의 history (지금까지의 결과를 넘겨받아 추가 학습을 진행할 수 있음)
  • 반환
    • history: (반복 횟수, 훈련 손실, 훈련 정확도, 검증 손실, 검증 정확도)의 2차원 넘파이 배열
def fit(net, optimizer, criterion, num_epochs, train_loader, test_loader, device, history):
	
	from tqdm.notebook import tqdm
	
	base_epochs = len(history)
	
	for epoch in range(base_epochs, num_epochs+base_epochs):
		train_loss = 0
		train_acc = 0
		
		net.train()
		count = 0
		
		for inputs, labels in tqdm(train_loader):
			count += len(labels)
			inputs = inputs.to(device)
			labels = labels.to(device)
			
		net.eval()
		count = 0
		
		for inputs, labels in test_loader:
		count += len(labels)
		inputs = inputs.to(device)
		labels = labels.to(device)

evaluate_history (학습 로그)

  • history를 인수로 받아와서 아래와 같은 역할을 수행한다
    • history 앞부분과 마지막 부분을 print 함수로 표시해서 학습 결과의 개요를 표시한다
    • 학습 곡선을 손실, 정확도 두 가지로 출력

show_images_labels (예측 결과 표시)

  • 모델이 올바르게 예측하고 있는지, 데이터 이미지 출력 등을 수행
  • 주로 데이터 시각화 및 디버깅 목적으로 사용
  • 인수
    • loader: 검증용 데이터로더
    • classes: 정답 데이터에 대응하는 라벨 값의 리스트 ([plane, car, bird]와 같은)
    • net: 사전에 학습이 끝난 모델 인스턴스 (None을 넘기면 정답 데이터만 표시됨)
    • device: 예측 계산에 사용하는 디바이스
if net is not None:
	predicted_name = classed[predicted[i]]
	# 정답인지 아닌지 색으로 구분
	if label_name == predicted_name:
		c = 'k'
	else:
		c = 'b'
	ax.set_title(label_name + ':' + predicted_name, c=c, fontsize=20)
else:
	ax.set_title(label_name, fontsize=20)

torch_seed (난수 고정)

  • 항상 동일한 결과를 얻기 위한 처리
def torch_seed(seed=123):
	torch.manual_seed(seed)
	torch.cuda.manual_seed(seed)
	torch.backends.cudnn.deterministic = True # GPU 사용 시 동일한 결과 얻을 수 있도록 추가 호출
	torch.use_deterministic_algorithms = True # GPU 사용 시 동일한 결과 얻을 수 있도록 추가 호출

 

9.6 데이터 준비

Transforms 정의

# transform1 - 정규화 및 1계 텐서화
transform1 = transforms.Compose([
	transforms.ToTensor(),
	transforms.Normalize(0.5, 0.5),
	transforms.Lambda(lambda x: x.view(-1)),
])

# transform2 - 정규화만 시킴
transform2 = transforms.Compose([
	transforms.ToTensor(),
	transforms.Normalize(0.5, 0.5),
])

데이터셋 정의

data_root = './data'

# 훈련 데이터셋 (1계 텐서 버전)
train_set1 = datasets.CIFAR10(
    root = data_root, train = True, 
    download = True, transform = transform1)

# 검증 데이터셋 (1계 텐서 버전)
test_set1 = datasets.CIFAR10(
    root = data_root, train = False, # 검증이므로 False
    download = True, transform = transform1)

# 훈련 데이터셋 (3계 텐서 버전)
train_set2 = datasets.CIFAR10(
    root =  data_root, train = True, 
    download = True, transform = transform2)

# 검증 데이터셋 (3계 텐서 버전)
test_set2 = datasets.CIFAR10(
    root = data_root, train = False, # 검증이므로 False
    download = True, transform = transform2)

데이터로더 정의

# 미니 배치 사이즈 지정
batch_size = 100

# 훈련용 데이터로더
# 훈련용이므로 셔플을 True로 설정
train_loader1 = DataLoader(train_set1, batch_size=batch_size, shuffle=True)

# 검증용 데이터로더
# 검증용이므로 셔플하지 않음
test_loader1 = DataLoader(test_set1,  batch_size=batch_size, shuffle=False) 

# 훈련용 데이터로더
# 훈련용이므로 셔플을 True로 설정
train_loader2 = DataLoader(train_set2, batch_size=batch_size, shuffle=True)

# 검증용 데이터로더
# 검증용이므로 셔플하지 않음
test_loader2 = DataLoader(test_set2,  batch_size=batch_size, shuffle=False) 

검증 데이터를 이미지로 표시

  • test_loader2에서 가져와서 처음 50개의 이미지를 표시
  • 모델을 사용하지 않고 정답을 타이틀로 함께 표시
# 정답 라벨 정의
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# 검증 데이터의 처음 50개를 출력
show_images_labels(test_loader2, classes, None, None)

9.7 모델 정의 (전결합형)

각 노드의 차원수 설정

# 입력 차원수는 3*32*32=3072
n_input = image1.view(-1).shape[0]

# 출력 차원수
# 분류 클래스의 수이므로 10
n_output = len(set(list(labels1.data.numpy())))

# 은닉층의 노드수
n_hidden = 128

# 결과 확인
print(f'n_input: {n_input}  n_hidden: {n_hidden} n_output: {n_output}')

모델 정의

# 3072입력 10출력 1은닉층을 포함한 신경망 모델

class Net(nn.Module):
    def __init__(self, n_input, n_output, n_hidden):
        super().__init__()
        
        self.l1 = nn.Linear(n_input, n_hidden) # 은닉층 정의(은닉층의 노드수 : n_hidden)
        self.l2 = nn.Linear(n_hidden, n_output) # 출력층의 정의
        self.relu = nn.ReLU(inplace=True) # ReLU 함수 정의
   
    def forward(self, x):
        x1 = self.l1(x)
        x2 = self.relu(x1)
        x3 = self.l2(x2)
        return x3

 

9.8 결과 (전결합형)

학습

  • fit 함수를 도입하여 학습 코드 구현을 한 줄로 간단하게 마무리 지을 수 있다
torch_seed() # 난수 설정

net = Net(n_input, n_output, n_hidden).to(device) # 모델 인스턴스 생성

criterion = nn.CrossEntropyLoss() # 손실 함수: 교차 엔트로피 함수
# => 해당 손실함수를 통해 분류 확률 계산

# 학습률
lr = 0.01

optimizer = optim.SGD(net.parameters(), lr=lr) # 최적화 함수: 경사 하강법

# 반복 횟수
num_epochs = 50

# 평가 결과 기록
history = np.zeros((0,5))

# 학습
history = fit(net, optimizer, criterion, num_epochs, train_loader1, test_loader1, device, history)

평가

evaluate_history(history)

 

 

  •  해석
    • 검증 데이터에 대한 그래프로부터 반복 횟수 30회 부근에서 학습이 정체하고 있음
    • 검증 데이터에 대한 정확도는 최대 53% 정도에 그침

 

9.9 모델 정의 (CNN)

CNN 모델 클래스 정의

class CNN(nn.Module):
  def __init__(self, n_output, n_hidden):
    super().__init__()
    self.conv1 = nn.Conv2d(3, 32, 3)
    self.conv2 = nn.Conv2d(32, 32, 3)
    self.relu = nn.ReLU(inplace=True)
    self.maxpool = nn.MaxPool2d((2,2))
    self.flatten = nn.Flatten()
    self.l1 = nn.Linear(6272, n_hidden)
    self.l2 = nn.Linear(n_hidden, n_output)

    self.features = nn.Sequential(
        self.conv1,
        self.relu,
        self.conv2,
        self.relu,
        self.maxpool)
    
    self.classifier = nn.Sequential(
       self.l1,
       self.relu,
       self.l2)

  def forward(self, x):
    x1 = self.features(x)
    x2 = self.flatten(x1)
    x3 = self.classifier(x2)
    return x3       
  • features → flatten → classifier

모델 개요 표시

print(net)

summary(net, (100, 3, 32, 32), depth=1)

 

9.10 결과 (CNN)

모델 초기화와 학습

# 난수 초기화
torch_seed()

# 모델 인스턴스 생성
net = CNN(n_output, n_hidden).to(device)

# 손실 함수: 교차 엔트로피 함수
criterion = nn.CrossEntropyLoss()

# 학습률
lr = 0.01

# 최적화 함수: 경사 하강법
optimizer = optim.SGD(net.parameters(), lr=lr)

# 반복 횟수
num_epochs = 50

# 평가 결과 기록
history2 = np.zeros((0,5))

# 학습
history2 = fit(net, optimizer, criterion, num_epochs, train_loader2, test_loader2, device, history2)

 

  • 전결합형은 데이터의 건수에 맞춰 가중치 행렬이 준비되었지만, CNN은 고정된 길이의 커널 행렬이 준비되어 있으므로, 어떤 크기의 입력이 들어와도 상관없다
  • 대신, classifier에서 선형함수가 시작되는 부분의 입력 차원수는 미리 계산해놓아야 함!! (변수로 처리할 수 있지 않나? -> 코드로 구현하여 가능하게 할 수 있음! 아니면, Adaptive 함수를 사용하는 것도 가능!)

여기 6272

평가

evaluate_history(history2)

 

  • 정확도가 66%로 향상함! (전결합형은 53%)
show_images_labels(test_loader2, classes, net, device)

 

 

⇒ 그러나, 여전히 정확도가 높지는 않음

⇒ 튜닝이 필요하다 (10장에서 튜닝 기법 내용이 나온다!)