계속 이어가겠습니다.
1. ptimizer.zero_grad()
기울기(gradient)를 0으로 초기화하는 단계입니다. 역전파(backward)를 실행할 때 그레디언트가 계속해서 누적되므로, 매번 새로운 학습 단계에서 기존의 그레디언트를 초기화해야 합니다. optimizer.zero_grad()는 이전 배치에서 계산된 그레디언트를 지워주는 역할을 합니다. 만약 이 작업을 하지 않으면, 이전 배치의 기울기가 이번 배치에 누적되어 잘못된 업데이트가 이루어질 수 있습니다.
2. outputs = model(inputs)
입력 데이터를 모델에 넣어 예측 값을 얻는 단계입니다. model(inputs)는 모델이 입력 데이터를 받아 순전파(forward pass)를 통해 예측값(outputs)을 생성하는 과정입니다. 여기서 inputs는 trainloader에서 가져온 배치(batch)입니다. trainloader는 DataLoader로서 데이터셋을 작은 배치 단위로 나눠서 학습시킵니다.
이 호출이 SimpleANN 클래스의 forward 메서드를 호출하는 역할을 합니다. 예를 들어, MNIST 데이터에서 배치 크기가 64라면, inputs는 (64, 1, 28, 28) 크기의 Tensor가 되고, 이것이 모델에 들어가게 됩니다.
3. loss = criterion(outputs, labels)
모델의 출력값과 실제 레이블(정답) 간의 손실(loss)을 계산하는 단계입니다. criterion은 손실 함수입니다. 여기서 사용된 손실 함수는 교차 엔트로피 손실 함수인 nn.CrossEntropyLoss()입니다.
outputs는 모델이 예측한 값이며, labels는 데이터셋의 실제 레이블(정답)입니다. 이 손실 함수는 예측값과 실제값 간의 차이를 계산하여, 얼마나 잘못된 예측을 했는지 나타냅니다. 손실이 클수록 모델의 예측이 틀렸다는 뜻입니다.
4. loss.backward()
역전파(backpropagation)를 통해 기울기를 계산하는 단계입니다. 손실 함수를 기준으로 각 파라미터에 대한 기울기를 계산합니다. 역전파는 손실 값이 각 가중치(weight)에 미치는 영향을 계산하는 과정입니다.
이 과정에서 각 파라미터에 대한 그레디언트(기울기)가 계산되며, 이후 optimizer.step()에서 이 값을 이용해 가중치가 업데이트됩니다. loss.backward()는 PyTorch가 자동 미분(autograd)을 사용해 모든 파라미터에 대해 그레디언트를 계산하도록 합니다.
5. optimizer.step()
계산된 기울기를 사용하여 파라미터를 업데이트하는 단계입니다. optimizer.step()은 최적화 알고리즘을 사용해 파라미터(가중치)를 업데이트합니다.
여기서는 SGD(확률적 경사 하강법)가 사용되고 있으며, 이 방법은 이전 단계에서 계산된 그레디언트를 바탕으로 가중치를 업데이트합니다. 이 과정에서 학습률(lr=0.01)과 모멘텀(momentum=0.9) 값이 영향을 미칩니다.
6. running_loss += loss.item()
배치마다 계산된 손실을 누적하는 단계입니다. loss.item()은 텐서로 된 손실 값을 파이썬 숫자로 변환하여, running_loss에 더해줍니다. 이렇게 하면 여러 배치에 걸쳐 평균 손실을 계산하거나, 손실의 변화를 추적할 수 있습니다.
7. if i % 100 == 99:
100번째 배치마다 손실을 출력하기 위한 조건문입니다. i % 100 == 99는 100번째 배치(미니배치)의 결과를 출력하겠다는 뜻입니다. 배치 인덱스 i가 99, 199, 299...일 때마다 결과를 출력합니다.
8. print(f'[Epoch {epoch + 1}, Batch {i + 1}] loss: {running_loss / 100:.3f}')
학습 도중 100번째 배치마다 손실 값을 출력합니다.현재까지의 평균 손실을 출력합니다. running_loss / 100은 100개의 배치 동안의 평균 손실 값을 계산한 것입니다.
출력 형태는 [에포크 번호, 배치 번호] 손실: 평균 손실 값입니다. 예를 들어, Epoch 1, Batch 100 loss: 0.215와 같은 식으로 출력됩니다.
9. running_loss = 0.0
출력 후 누적된 손실 값을 초기화합니다. 평균 손실을 출력한 후, running_loss를 0으로 초기화하여 다음 100개의 배치 동안의 손실 값을 새로 계산합니다.
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total:.2f}%')
1. 변수 초기화
correct = 0
total = 0
correct: 모델이 올바르게 예측한 데이터 수를 저장할 변수입니다.
total: 테스트 데이터의 총 개수를 저장할 변수입니다.
2. torch.no_grad() 사용
with 문을 사용하면 블록 내에서 발생하는 예외를 효과적으로 처리할 수 있습니다. 자원이 잘못 사용될 경우 발생할 수 있는 오류를 피할 수 있습니다.
PyTorch의 컨텍스트 관리자로, 이 블록 내에서 기울기 계산을 비활성화합니다. 이는 메모리 사용을 줄이고, 불필요한 계산을 방지하여 평가 속도를 높입니다. 이 블록 내에서는 텐서의 requires_grad 속성이 False로 설정됩니다.
여기서 with을 쓴 이유는, gradient 계산을 비활성화하여 메모리 사용량을 줄이고, 성능을 최적화하며, 코드의 의도를 명확히 하기 위해서입니다. 이를 통해 모델의 추론 단계에서 불필요한 연산을 방지하고, 더 빠르고 효율적인 실행이 가능하게 됩니다.
3. for data in testloader:
testloader에서 배치 단위로 데이터를 가져옵니다. data는 각 배치에 대한 이미지와 레이블을 포함하는 튜플입니다.
4. images, labels = data
images: 입력 이미지 데이터
labels: 해당 이미지에 대한 정답 레이블
5. outputs = model(images)
입력 이미지를 모델에 통과시켜 예측 결과를 얻습니다. outputs는 각 클래스에 대한 예측 점수(logits)를 포함하는 텐서입니다.
6. _, predicted = torch.max(outputs.data, 1)
torch.max(outputs.data, 1)는 각 이미지에 대해 최대값을 찾고, 그 최대값의 인덱스를 반환합니다. 여기서 1은 두 번째 차원(클래스 차원)을 기준으로 최대값을 찾도록 지정하는 것입니다. predicted는 예측된 클래스의 인덱스(예: 0부터 9까지의 숫자)를 담고 있습니다.
_, predicted = torch.max(outputs.data, 1)에서 첫 번째 반환값(최대값)은 필요 없고, 오직 클래스의 인덱스인 predicted만 필요할 때 _를 사용합니다. _를 사용하는 것은 많은 파이썬 프로그래머들 사이에서 관례로 자리잡혔습니다. 이는 누군가 코드에서 _를 보면 "이 변수는 사용되지 않음"을 이해할 수 있습니다.
torch.max 함수는 두 개의 값을 반환합니다. 첫 번째 값은 각 열의 최대값이고, 두 번째 값은 그 최대값의 인덱스입니다. outputs.data는 모델의 출력으로, 각 클래스에 대한 로짓(logit) 값을 포함하고 있습니다. 1은 차원을 지정하는 인자로, 여기서는 열 방향(클래스 차원)으로 최대값과 인덱스를 찾도록 지정합니다.
그렇게 하면 반환이 되는 값이 2개가 있는데, 첫 번째 값: 각 샘플에 대해 최대 로짓 값 (예: 신뢰도) 두 번째 값: 그 최대값을 가지는 클래스의 인덱스 (예: 예측된 클래스)
따라서, _는 반환된 첫 번째 값을 사용하지 않겠다는 의미입니다. 즉, 최대값(신뢰도 값)은 필요 없고, 오직 예측된 클래스 인덱스만 필요하다는 것을 나타냅니다.
7. total += labels.size(0)
labels.size(0): 현재 배치의 레이블 수를 반환합니다. 이 값은 현재 배치에 포함된 이미지의 수와 같습니다.
여기서 size(0)은 PyTorch에서 텐서의 특정 차원의 크기를 반환하는 메서드입니다. 이는 보통 행(row)의 수를 의미합니다.
total: 이 값을 더하여 전체 테스트 데이터의 수를 업데이트합니다.
8. correct += (predicted == labels).sum().item()
predicted == labels는 예측된 값과 실제 레이블이 일치하는지 여부를 나타내는 불리언 텐서를 반환합니다.
sum()은 일치하는 경우의 수를 세고, item()을 통해 스칼라 값으로 변환하여 correct에 더합니다.
CNN
CNN은 딥러닝의 개념중 하나로써, ANN과 다르게 이미지와 같은 2차원 데이터의 특징을 효과적으로 추출하기 위해 설계된 신경망입니다. ANN에서 완전 연결 층(FCL)를 썼다면, CNN에서는 합성곱 층(Convolutional Layer), 풀링 층(Pooling Layer), 완전 연결 층(Fully Connected Layer)으로 구성됩니다.
합성곱 층 (Convolutional Layer) ->입력 이미지에 필터를 적용하여 특징 맵을 생성합니다
먼저 필터(커널)에 대해 알아보겠습니다
필터(또는 커널)은 이미지를 분석하기 위한 작은 크기의 행렬(수학에서 사용되는 "Matrix(매트릭스)"를 의미해요. 행렬은 숫자들이 행과 열로 배열된 직사각형 모양의 표와 같은 개념입니다.)입니다. 보통 크기는 3x3, 5x5, 또는 7x7 등으로 설정되는데, 이는 필터가 동시에 몇 개의 픽셀을 분석할지를 나타냅니다.
이미지에는 각 픽셀마다(X*X)고유한 값이 있습니다. 그리고 곧 그 값은 이미지에 대한 크기에 따라 결정됩니다.
예를 들어, 28*28픽셀의 이미지에서 필터란, 3*3으로 설정을 한다면, 가로 3 세로 3 픽셀에 해당하는 작은 기계가 28*28 픽셀 이미지 위에서 처음부터 오른쪽으로 계속해서 3*3에 들어온 값들을 계산해서, 전체 픽셀을 계산을 하는데, 이때 이 전체 값들을 특징 맵이라고 합니다.
예를 들면,
[ 1 2 3 ]
[ 4 5 6 ]
[ 7 8 9 ]
이렇게, 28x28 크기의 이미지가 있으면, 3x3 필터(커널)는 그 큰 이미지 안에서 작은 창처럼 동작합니다. 필터가 이미지 위를 이동하면서, 겹치는 3x3 크기의 영역을 처리하는 것입니다.
풀링 층 (Pooling Layer) -> 하지만 만약 이미지가 너무 크다면, 처리하는 데, 시간이 오래 걸릴 수 있습니다. 그렇기 때문에 전처리라고 생각하면 편합니다. 특징 맵의 크기를 줄이지만, 중요한 정보 압축하여 정보 보호와 학습을 효율적으로 만드는데 사용됩니다.
Max Pooling(최대 풀링)과 Average Pooling(평균 풀링) 두 가지 방식이 사용됩니다.
Max Pooling -> 풀링 층은 이미지를 작은 창(주로 2x2 또는 3x3)으로 나누어 해당 영역의 특징을 요약합니다. 일반적으로는 이미지의 크기를 줄이면서 중요한 정보는 남기고, 불필요한 세부 사항을 제거하는 역할을 합니다.
예를 들어,
입력 이미지 (4x4):
[ 1 2 3 4 ]
[ 5 6 7 8 ]
[ 9 10 11 12 ]
[13 14 15 16 ]
을 2x2로 바꾼다고하면, [1,2,5,6], [3,4,7,8], [9,10,13,14], [11,12,15,16]으로 구역이 나뉘어지는데, 각 파트마다 가장 큰 값인 6,8,14,16으로 값을 축소한다는 뜻입니다.
[ 6, 8]
[14,16]
2x2의 창으로 압축하면, 그 계산된 값들중 가장 큰 값 하나만을 선택해 영역을 대표하게 합니다.
Average Pooling -> 영역의 값을 모두 더한 후 평균값을 선택하는 방식입니다.
만약 2x2 창을 사용하면, 2x2 영역의 4개 값의 평균을 계산해서 그 값을 대표로 삼습니다.
완전 연결 층 (Fully Connected Layer) -> 완전 연결 층 (Fully Connected Layer)는 신경망의 마지막 단계에서 사용되는 층으로, CNN이 추출한 특징들을 바탕으로 최종적으로 예측을 수행하는 중요한 부분입니다.
완전 연결 층의 역할
특징 맵을 1차원 벡터로 변환: CNN에서 나온 2D 형태의 특징 맵을 flatten(평탄화) 하여 1차원으로 변환합니다. 이렇게 변환된 벡터는 이제 모델이 다음 단계로 쉽게 처리할 수 있는 형태가 됩니다. 예를 들어, (7x7x64)의 3D 특징 맵을 1D 벡터인 (7 * 7 * 64 = 3136) 크기의 벡터로 변환합니다.
모든 뉴런이 서로 연결: 완전 연결 층에서는 이전 층의 모든 뉴런이 다음 층의 모든 뉴런과 연결됩니다. 각각의 뉴런은 이전 층의 뉴런 값과 학습된 가중치를 곱하고, 편향 값을 더해 가중합을 계산합니다.
결과를 예측: 최종적으로 완전 연결 층은 주어진 입력 특징들에 대해 특정 클래스에 속할 확률을 계산합니다. 마지막에 softmax 함수나 sigmoid 함수 같은 활성화 함수를 적용해, 각 클래스에 대한 확률 값으로 변환합니다.
여기서 바뀐 코드들만 보겠습니다.
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(3, 32, 3, padding=1) # 입력 채널 3, 출력 채널 32, 커널 크기 3x3
self.pool = nn.MaxPool2d(2, 2) # 풀링 크기 2x2
self.conv2 = nn.Conv2d(32, 64, 3, padding=1) # 입력 채널 32, 출력 채널 64, 커널 크기 3x3
self.fc1 = nn.Linear(64 * 8 * 8, 512) # 완전 연결 층
self.fc2 = nn.Linear(512, 10) # 출력 층 (10개의 클래스)
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x)))
x = self.pool(torch.relu(self.conv2(x)))
x = x.view(-1, 64 * 8 * 8) # 플래튼
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x
1. self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
nn.Conv2d ->PyTorch에서 2차원 합성곱 연산을 수행하는 모듈입니다. 주로 이미지와 같은 2차원 데이터를 처리하는 데 사용됩니다. CNN(합성곱 신경망)의 핵심 구성 요소로, 입력 이미지에 필터(커널)를 적용하여 특징(특징 맵)을 추출하는 역할을 합니다.
구조:
nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0)
참고로 stride는 필터가 얼마나 크게 점프하는지를 조정해 출력 크기를 줄이고, 계산량을 줄이기 위한 것입니다.
그리고, Padding은 경계 부분의 정보 손실을 줄여, 필터가 이미지 가장자리에서도 유용한 정보를 추출할 수 있도록 하는 것입니다. 여기서는 padding을 1로 줬는데, 그러면 30x30이 됩니다. 왜냐하면 한 칸이 아니라 전 구역으로 1칸을 늘려주기 때문입니다.
첫 번째 합성곱 레이어(conv1)를 정의합니다.
입력 채널 수: 3 (RGB 이미지의 경우)
출력 채널 수: 32 (32개의 필터)
커널 크기: 3x3 padding=1은
입력 이미지의 가장자리에 1픽셀의 패딩을 추가하여 출력 크기를 유지합니다.
2. self.pool = nn.MaxPool2d(2, 2)
nn.MaxPool2d는 PyTorch에서 2D 최대 풀링을 수행하는 클래스입니다. 주로 이미지 데이터와 같이 2차원 형태의 입력을 처리할 때 사용됩니다. 최대 풀링은 필터를 사용하여 입력의 각 지역에서 최대 값을 선택합니다. 이로 인해 입력 데이터의 크기를 줄이고, 특성의 중요 정보를 보존할 수 있습니다.
인자로는(kernel_size, stride)이 있는데,
kernel_size: 풀링 필터의 크기를 정의합니다. 위의 코드에서 2는 2x2 크기의 필터를 의미합니다. 즉, 2x2 영역의 픽셀들 중 최대 값을 선택합니다.
stride: 필터가 이동하는 간격을 정의합니다. 위의 코드에서 2는 필터가 2픽셀씩 이동한다는 뜻입니다. 이는 풀링 결과의 크기를 절반으로 줄이는 효과를 줍니다.
즉, 2x2의 커널로 계산해서 줄여주고 바로 2칸씩 이동한다는 의미입니다. 참고로 최종적으로 2x2로 만들어 주는것이 아닌, 2x2씩 한번씩 줄여주는 역할을 합니다.
이 코드는 지금 그냥 앞으로 self.pool을 쓰면 계산을 해준다는 의미로 해석할 수 있습니다.
오늘은 여기까지..