import 및 데이터셋 불러오기
class. SimpleANN:
SimpleANN이라는 클래스에서 신경망 모델을 정의하고 있다.
class.SimpleANN(nn.Module)
이 클래스는 nn.Module이라는 부모클래스를 상속받고 있다.
nn.module이란? pytorch에서 모델을 만들 때 쓰는 기본 클래스로, 신경망 정의에 필요한 각종 기능들(파라미터 관리, 순전파, 역전파에서 그래디언트 계산 등)을 제공해줌
def__init__(self): 클래스의 생성자
super(SimpleANN, self).__init__()
super() 를 사용해서 부모클래스를 호출하고,
호출한 부모클래스(= nnModule )의 __init__매서드를 호출해 부모클래스를 초기화한다.
부모클래스를 초기화해야 nn.Module의 여러 기능들이 제대로 동작한다.
self.fc1=nn.Linear(28*28.128)
우리가 가져온 MNIST데이터는 28*28크기의 2D 이미지이다. 이 이미지를 1차원 벡터로 만들고 (28*28=784), 이 1차원 벡터를 받아 128개의 노드로 변환함
1차원으로 만드는 이유? 아직 이 단계의 신경망은 선형계층으로 이미지를 처리하고 있다. 나중에 활성화함수를 통해 비선형으로 바꿔준다.
self.fc2=nn.Linear(128,64):
self.fc3=nn.Linear(64,10):
128개로 받은 것을 64개로, 64개로 받은 것을 10개로 변환해서 최종 출력노드는 10개
def forward(self.x):
이제 SimpleANN 클래스 내의 forward라는 함수를 정의할건데,
x=x.view(-1.28*28)
28*28의 2D 이미지를 1차원 벡터로 펼쳐주는 과정
view()함수는 텐서를 다른 모양으로 재구성하는 함수 즉 텐서의 차원 변경
view함수의 사용예시
x=torch.randn(2,3,4)
re_x=x.view(2,12)
-> 3차원 텐서(2*3 * 4)를 2차원 텐서(2 * 12)로 변경
x=torch.randn(64,1,28,28)
re_x=x.view(-1,28*28)
-> 배치 크기 64, 채녈1, 28*28의 2차원 이미지를 1차원 벡터(28*28=784)로 변경하는데, 배치크기는 64로 유지
x=torch.randn(4.6)
re_x=x.view(-1,3)
-> 2차원 텐서(4 * 6)를 2차원 텐서(8 * 3)로 변경
-1은 자동으로 크기를 맞춘다는 뜻이므로, 4*6=24에 맞게 차원을 자동으로 결정함
참고) 텐서란?
데이터를 0차원~n차원까지 다차원 배열 형태로 표현하는 객체
0차원 텐서=스칼라=숫자 하나 ex) 9
1차원 텐서=벡터=숫자들의 나열 ex)[1,2,3]
2차원 텐서=행렬=숫자의 2차원 배열 ex)[[1,2,3],[4,5,6]]
3차원 텐서= 3차원 배열 ex) [[[1,2],[3,4]], [[5,6],[7,8]]]
이미지 데이터는 3차원 텐서로 표현된다. 예를들어 MNIST는 28*28의 2D이미지이므로 (28,28,1)로 표현된다.
x=torch.relu(self.fc1(x))
x=torch.relu(self.fc2(x))
첫 번째 선형계층에 데이터를 통과시킨 뒤 (=self.fc1(x))
이 결과를 다시 relu함수에 넣음 -> 선형이 비선형이 되었다.
두번째 선형계층에도 마찬가지로 relu함수 적용
self.fc3(x):
마지막 선형계층은 relu함수나 다른 활성화함수를 적용하지 않는다.
이 값을 로짓이라고 한다. 로짓은 확률로 변환되지 않은 원시적인 출력값으로 음수나 매우 큰 값을 가질 수 있다.
나중에 여기에 소프트맥스 함수를 적용해 로짓을 확률로 변환해준다.
변환된 확률은 해당클래스에 속활 확률이 된다.
ex)
로짓: [2.3, -1.5, 0.7] -> 소프트맥스 적용 후 : [0.7, 0.05, 0.25]
이는 첫번째 클래스에 속할 확률이 70%, 두 번째 클래스에 속할 확률은 5%, 세번째 클래스에 속할 확률은 25%라는 뜻
model = SimpleANN()
앞에서 정의한 SimpleANN이라는 클래스에 따라 모델을 생성
criterion = nn.CrossEntropyLoss()
(nn=torch.nn 맨 위에 import를 보면 torch.nn을 nn으로 불러왔다. torch.nn안에는 여러 클래스와 함수들이 있고, nn.module, nn.CrossEntropyLoss도 모두 torch.nn모듈 안에 있는 클래스이다.)
CrossEntropyLoss는 예측값과 실제값(=라벨)의 차이를 계산해서(=이 차이를 계산하는 것이 바로 손실함수) 모델이 얼마나 잘못 예측했는지 알려준다.
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
SGD는 경사하강법의 일종
경사하강법이란?
특정 함수가 줄어드는 방향으로 변수값을 업데이트하는 방법
그러니까 경사하강법을 사용하면 loss function(손실함수)이 줄어드는 방향으로 가중치가 업데이트된다.
Ir은 학습률, 모멘텀은 관성이다. 모멘텀으로 학습속도를 높이고 지역최적값에 갇히지 않게 할 수 있다.
for epoch in range(10):
학습데이터 전체로 한 번 학습하는 과정을 1에포크라고 한다. 이 경우 10번의 학습을 실행한다.(10 에포크)
running_loss = 0.0
학습과정중 미니배치마다 누적되는 손실값을 저장하는 변수를 running_loss라고 했다. 나중에 여기다가 계산된 손실값을 계속 더해줄 예정이다.
for i, data in enumerate(trainloader, 0):
inputs, labels = data
trianloader는 갑자기 어디서 나온 것일까?
사실 데이터셋 로드할 때 trainloader가 뭔지 정의했다.
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
이 정의에 따르면 trainset는 MNIST데이터셋을 가져와서 transform을 적용해 텐서로 변환한 것이다.
trainloader는 trainset을 64개씩 한 번에 가져오는데, 무작위로 섞어서 가져온다. 즉 미니배치의 사이즈=64
enumerate 는 파이썬 내장함수로, 반복문에서 인덱스와 데이터를 동시에 가져온다.
그러니까 enumerate(trainloader,0)은 trainloader에서 데이터를 하나씩 가져오면서 그 데이터에 0부터 시작하는 인덱스(여기서는 i)도 붙이는 것이다. 이렇게 해야 반복문 내에서 몇 번째 인지를 알 수 있다.
data는 (inputs, labels)라는 하나의 배치 데이터를 의미한다. trainloader로부터 입력데이터(input)과 그에 해당하는 (label)을 가져온다.
optimizer.zero_grad()
기울기를 초기화하는 함수
기울기란? 손실함수가 가중치에 따라 얼마나 변하는지를 계산한 것으로 모델의 출력이 입력에 얼마나 민감하게 반응하는지를 나타냄
학습과정에서 기울기가 계속 누적되지 않게 초기화해준다.
loss.backward()
손실값에 대한 각 파라미터의 기울기를 계산한다. 이 기울기를 이용해 가중치를 업데이트한다.
optimizer.step()
가중치 업데이트 함수
if i % 100 == 99:
print(f'[Epoch {epoch + 1}, Batch {i + 1}] loss: {running_loss / 100:.3f}')
running_loss = 0.0
i%100==99 : i를 100으로 나눴을 때 나머지가 99인 경우 즉, i==99,199,299..
i=0에서 시작했으므로 100번쨔 ,200번째, 300번째를 의미한다.
running_loss는 위에서도 설명했듯 미니배치마다 손실을 더한 값이다. 100번째 배치에서 평균값을 계산한 뒤 다시 running_loss를 0.0으로 초기화시키고 있다.
참고로 .3f는 반올림해서 소수점 3자리까지 출력한다는 뜻이다. ex)3.141592.. ->3.142
correct = 0
total = 0
correct는 맞춘 예측 개수를 저장할 변수이고, total은 전체 테스트 샘플의 개수를 저장하는 변수이다.
with torch.no_grad():
기울기 계산 비활성화 함수
기울기는 학습할 때 사용되는 것이지 평가할때는 계산할 필요가 없다. 그래서 비활성화 해둬야 메모리도 세이브하고 속도도 빨라진다.
for data in testloader:
images, labels = data
outputs = model(images)
testloader도 데이터셋 불러올 때 정의했다. trainset과 분리된 testset으로부터 가져온 데이터이다.
마찬가지로 여기서도 데이터를 가져와서 images와 labels를 얻고
model에 images를 넣으면 model이 해당 이미지에 대한 예측값을 반환한다.(output)
model이 뭐냐면 model=SimpleANN() 에 있는 그 model이다. 위에서 열심히 학습해서 똑똑해진 model
_, predicted = torch.max(outputs.data, 1)
torch.max는 다음 두 가지 값을 반환한다.
(최댓값, 최댓값이 위치한 인덱스)
예를 들어 outputs=model(images)에서 모델이 images를 보고 다음과 같은 outputs를 냈다.
[0.1, 0.05, 0.02, 0.7, 0.05, 0.03, 0.01, 0.02, 0.01, 0.01]
이는 이 이미지가 클래스0에 속할 확률이 10%, 클래스1에 속할 확률이 5%... 클래스3에 속할 확률이 70%... 클래스9에 속할 확률이 1%라는 뜻이다. 즉 이 이미지는 클래스3에 속한다고 판단한 것
torch.max는 이것을 보고 (0.7, 3) 을 반환한다.
이 때 우리는 0.7(70%)이라는 확률 자체는 필요없고 3에 속한다는 정보만 필요하다. 그래서 _,predicted로 받아서 앞에 0.7은 무시하고 3만 받는 것이다. 즉 이 경우 predicted에 3이 저장됨
'_'는 파이썬에서 변수명을 무시하고 싶을 때 쓰는 관용적 표현이다.
_,predictd= torch.max(output.data)까지는 알겠는데, 1은 뭘까?
MNIST 에서 모델의 출력은 2차원 텐서구조를 갖는데, 0차원은 배치(batch), 1차원은 클래스 이렇게 총 2차원이다.
outputs = [ [0.1, 0.3, 0.6], [0.2, 0.5, 0.3] ]
이렇게 생긴 output이라고 하면 0차원은 첫번째 이미지에 대한 예측인 [0.1, 0.3, 0.6], 두번째 이미지에 대한 예측인 [0.2, 0.5, 0.3] 이 두 개이다.
1차원은 각 이미지 내에서 클래스에 속할 확률이다. 즉 0.1, 0.3, 0.6이 1차원이다.
그러니까 torch.max는 이 1차원 내에서 가장 높은 것을 찾아야 어느 클래스에 속할지 알게 되는 것이다.
이게 바로 torch.max(output.data, 1) 의 의미
total += labels.size(0)
labels에는 이미지가 어디에 속하는지에 대한 정답이 있다. 이미지와 정답은 일대일로 대응하므로, 이 정답의 개수는 이미지의 개수와 동일하다.
size()는 텐서의 크기를 반환한다. size(0)은 1차원 텐서의 크기이다.
예시
labels = torch.tensor([3, 1, 4, 0, 2])
print(labels.size()) # 출력: torch.Size([5])
print(labels.size(0)) # 출력: 5
이미지가 5개 있고, 각각 3번, 1번, 4번, 0번, 2번 클래스에 속한다는 정답이 있다.
이 라벨의 크기는 5이고, 이를 통해 5개의 이미지가 있음을 추정할 수 있다.
labels = torch.tensor([[3, 1], [4, 0], [2, 5]])
print(labels.size()) # 출력: torch.Size([3, 2])
print(labels.size(0)) # 출력: 3 (첫 번째 차원의 크기)
print(labels.size(1)) # 출력: 2 (두 번째 차원의 크기)
MNIST데이터와는 다르지만, 예를 들어 3개의 이미지가 있고 각 이미지에 두 개의 라벨이 있다고 해보자
size(0)을 하면 첫번째 차원(=배치의 크기)인 3이 나온다. size(1)을 하면 두번째 차원인 라벨의 개수가 나온다.
즉 total += labels.size(0) 이란,
labels.size(0)= 이미지의 개수 이므로, 처리한 이미지를 다 더해서 이미지의 총 개수를 알려주는 코드이다.
correct += (predicted == labels).sum().item()
예측한값(predicted)과 실제 정답(labels)이 일치하는 것을 합하는데 ->sum()
이렇게 합해진 것은 텐서의 형태로 나온다. 이 때 안에 있는 숫자 하나만 뽑아 파이썬 기본 자료형으로 변환해주는게 바로 item()이다. 즉 이 코드는 맞게 예측한 개수를 다 합해주는 코드
참고로 딕셔너리에서 키-벨류 쌍을 얻는 메서드는 items()였다.
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total:.2f}%')
(맞춘이미지개수(correct)/전체이미지개수(total)) *100 이것을 소숫점 둘째자리까지 나타내고 뒤에 % 붙여서 출력
ex 10000개 중 9500개 맞았으면 9500/10000 *100 -> 정확도: 95%
'머신러닝' 카테고리의 다른 글
RNN의 이해 (0) | 2024.10.21 |
---|---|
CNN의 이해 (0) | 2024.10.18 |
간단한 수학 통계 지식(1) (5) | 2024.10.16 |
k-means clustering (0) | 2024.10.15 |
선형회귀, 다항회귀, 로지스틱 회귀 (0) | 2024.10.14 |