서술형 · 설명 연습
문제를 먼저 종이에 써 보고, 눌러서 답을 확인하세요. 공식은 정의 → 왜 → 예시·숫자. 노란 칸은 꼭 들어가야 할 키워드예요.
1. 자식 클래스의 __init__() 안에서 super().__init__()을 호출해야 하는 이유를 설명하시오.
자식이 __init__을 새로 쓰면 부모의 __init__은 자동으로 안 불려요. 그러면 부모가 만들어 주던 값(예 self.name)이 없어서 나중에 쓸 때 에러가 나요. 그래서 super().__init__(...)으로 부모의 초기화를 직접 불러 줘요.
자식 클래스가 __init__()을 오버라이딩하면 부모의 __init__()은 자동으로 호출되지 않는다. 그러면 부모가 초기화하는 속성(예 name)이 만들어지지 않아 이후 사용할 때 AttributeError가 난다. 따라서 자식 __init__() 안에서 super().__init__(...)을 호출해 부모 초기화를 직접 실행해야 한다. nn.Module을 상속한 모델에서도 layer와 파라미터를 관리할 준비를 하려고 맨 먼저 호출한다.
2. model(x)를 호출하면 forward()가 실행되는 과정을 설명하시오.
우리가 만든 모델 클래스에는 __call__이 없어요. 그래서 부모인 nn.Module의 __call__이 대신 불려요. 이 __call__이 필요한 준비 작업(hook 등)을 한 뒤 우리가 쓴 forward를 실행해요. forward를 직접 부르면 이 준비 작업을 건너뛰니까 model(x)로 불러요.
model은 MyModel의 인스턴스이고 MyModel에는 __call__이 없으므로 부모 nn.Module의 __call__()이 상속되어 실행된다. 이 __call__()은 hook 실행과 상태 관리에 필요한 처리를 한 뒤 자식 클래스에 정의된 forward()를 호출한다. 그래서 model.forward(x)를 직접 부르거나 __call__을 재정의하지 않고 model(x)로 호출한다.
3. nn.Module을 상속한 모델에서 __init__()과 forward()의 역할을 쓰고, layer를 self에 저장해야 하는 이유를 설명하시오.
__init__은 모델에 필요한 부품(층, 학습할 값)을 만들어 등록하는 곳이고, forward는 입력이 들어왔을 때 그 부품을 어떤 순서로 쓸지 적는 곳이에요. 부품을 self에 저장해야 PyTorch가 학습 대상으로 알아봐요. forward 안에서 매번 새로 만들면 등록이 안 돼서 업데이트가 안 돼요.
__init__()은 모델의 구성 요소(layer, 파라미터)를 정의하고 등록하며, forward()는 입력에서 출력까지의 계산 흐름을 정의한다. layer를 self의 attribute로 저장해야 nn.Module이 이를 하위 모듈과 파라미터로 인식해 parameters()에 포함하고 gradient 계산과 weight 업데이트 대상이 된다. forward 안에서 layer를 새로 만들면 등록되지 않아 학습되지 않는다.
4. 한 번의 학습 step을 이루는 다섯 단계를 순서대로 쓰고 각 역할을 설명하시오.
1) zero_grad로 지난번 기울기를 지워요. 2) forward로 예측해요. 3) 예측과 정답을 비교해 loss를 구해요. 4) backward로 기울기를 계산해요. 5) step으로 기울기를 써서 weight를 고쳐요. 이 다섯 줄을 batch마다 반복해요.
optimizer.zero_grad()로 이전 step에서 쌓인 gradient를 초기화하고, prediction = model(x)로 예측값을 계산하며, loss = criterion(prediction, target)으로 예측과 정답의 차이를 구한다. loss.backward()로 각 파라미터의 gradient를 계산해 .grad에 저장하고, optimizer.step()으로 저장된 gradient를 이용해 파라미터를 갱신한다. 이 단계를 batch마다 반복한다.
5. loss.backward()와 optimizer.step()의 차이를 쓰고, gradient가 누적되기 때문에 zero_grad가 필요한 이유를 설명하시오.
backward는 기울기를 계산해서 .grad에 적어 두기만 해요. weight는 안 바꿔요. step이 그 기울기를 읽어서 weight를 실제로 고쳐요. 그런데 backward를 부를 때마다 기울기가 덮어써지지 않고 더해져요. 안 지우면 지난번 값이 섞여 틀린 방향으로 가서, 매번 zero_grad로 지워요.
loss.backward()는 chain rule로 gradient를 계산해 각 파라미터의 .grad에 저장(누적)할 뿐 파라미터 값은 바꾸지 않는다. optimizer.step()은 저장된 gradient를 이용해 파라미터 값을 갱신하며 gradient를 계산하거나 초기화하지는 않는다. gradient는 기존 .grad에 더해지므로 매 step 시작에 zero_grad()로 초기화하지 않으면 이전 step의 gradient가 섞여 잘못 업데이트된다.
6. 일반 텐서와 nn.Parameter의 차이를 쓰고, 모델 안에서 일반 텐서를 weight로 쓰면 생기는 문제를 설명하시오.
둘 다 requires_grad를 켜면 기울기는 계산돼요. 하지만 nn.Parameter만 모델에 자동 등록돼서 parameters()에 나와요. 일반 텐서는 등록이 안 되니까 optimizer에 안 넘어가고, 그러면 기울기는 있어도 weight가 업데이트되지 않아요.
nn.Parameter는 nn.Module의 attribute로 대입하면 자동으로 모델에 등록되고 기본적으로 requires_grad=True이며 parameters()와 named_parameters()에 나타난다. 일반 텐서는 requires_grad=True여도 등록되지 않아 parameters()에 포함되지 않으므로 optimizer에 전달되지 않고 gradient가 계산되어도 업데이트되지 않는다.
7. 샘플 10개, batch_size=4인 DataLoader를 사용할 때 한 epoch의 batch 구성을 쓰고, Dataset·DataLoader·batch·epoch의 관계를 설명하시오.
Dataset은 샘플을 번호로 꺼낼 수 있는 데이터 모음이고, DataLoader는 거기서 샘플을 4개씩 묶어 batch로 내주는 도구예요. 10개를 4개씩 자르면 4, 4, 2개로 세 묶음이에요. 이 세 묶음을 다 쓰면 데이터를 한 바퀴 돈 거라서 1 epoch이고, 그동안 weight는 세 번 업데이트돼요.
Dataset은 인덱스로 (입력, 정답) 샘플을 꺼낼 수 있는 데이터 모음이고, DataLoader는 Dataset에서 샘플을 모아 지정한 batch_size로 묶어 반복해서 내준다. 샘플 10개, batch_size=4, drop_last=False이면 4개, 4개, 2개의 batch 3개가 되고 len(loader)는 3이다. 모든 batch를 한 번 사용하면 학습 데이터 전체를 한 번 사용한 것이므로 1 epoch이며 이때 weight 업데이트는 3번 일어난다.
8. 브로드캐스팅의 규칙을 설명하고, (2, 3) + (3,)은 되는데 (2, 3) + (2,)는 왜 안 되는지 쓰시오.
두 모양을 오른쪽 끝에 맞추고 뒤에서부터 하나씩 비교해요. 크기가 같거나, 한쪽이 1이거나, 한쪽에 그 차원이 없으면 돼요. (2,3)과 (3,)은 3과 3이 같고 앞은 비어 있어서 돼요. (2,3)과 (2,)는 3과 2가 만나는데 같지도 1도 아니라서 안 돼요.
서로 다른 shape의 텐서를 연산할 때 PyTorch가 크기를 자동으로 맞추는 기능이다. 두 shape을 오른쪽 끝에 맞추고 마지막 차원부터 비교해 각 차원이 같거나 둘 중 하나가 1이거나 한쪽에 그 차원이 없어야 한다. (2,3)+(3,)은 3=3이고 앞 차원이 없어 가능하지만 (2,3)+(2,)는 3과 2가 같지도 1도 아니어서 불가능하다.
9. 같은 코드를 GPU가 있는 컴퓨터와 없는 컴퓨터에서 모두 돌리려면 어떻게 작성해야 하는지 쓰고, device와 관련해 주의할 점 두 가지를 설명하시오.
device를 'cuda'로 못 박지 않고 torch.cuda.is_available()로 물어봐서 GPU가 있으면 cuda, 없으면 cpu로 정해요. 주의할 점은 두 가지예요. 하나는 모델과 데이터가 같은 device에 있어야 한다는 것이고, 다른 하나는 x.to(device)를 하면 x = x.to(device)처럼 다시 저장해야 한다는 거예요.
device = "cuda" if torch.cuda.is_available() else "cpu"로 환경에 맞게 device를 정하고 텐서와 모델을 .to(device)로 옮긴다. 주의할 점은 첫째 텐서와 모델이 반드시 같은 device에 있어야 하며 다르면 연산에서 에러가 난다는 것, 둘째 .to(device)는 옮겨진 새 텐서를 반환하므로 x = x.to(device)처럼 다시 저장해야 한다는 것이다.
10. 딥러닝 모델이 학습으로 weight를 고쳐 가는 과정을 gradient와 학습률을 사용해 설명하시오.
처음엔 weight를 랜덤으로 채워서 예측이 틀려요. 예측과 정답의 차이가 loss예요. 각 weight를 조금 바꿨을 때 loss가 어느 쪽으로 변하는지가 gradient예요. 그래서 w ← w − 학습률 × gradient로 loss가 줄어드는 쪽으로 조금씩 옮겨요. 학습률은 한 번에 옮기는 폭이라 보통 아주 작게 해요. 이 과정을 데이터로 계속 반복해요.
weight를 랜덤으로 초기화한 뒤 데이터를 모델에 넣어 예측값을 구하고 정답과의 차이인 loss를 계산한다. 각 weight에 대해 loss의 변화율인 gradient를 구하고 w ← w − lr × gradient로 loss가 줄어드는 방향으로 업데이트한다. gradient가 양수이면 w를 줄이고 음수이면 늘린다. 학습률 lr은 한 번에 움직이는 크기를 정하며 보통 0.001처럼 작게 쓴다. 이를 여러 batch와 epoch 동안 반복해 loss가 작은 값으로 수렴하게 한다.
11. reshape()와 view()의 차이, cat()과 stack()의 차이를 각각 설명하시오.
reshape와 view는 둘 다 원소 수는 그대로 두고 모양만 바꿔요. view는 메모리를 같이 쓰는 게 보장되지만 쓸 수 없는 경우가 있고, reshape는 어디서나 되지만 공유가 보장되지 않아요. cat은 있는 차원에 이어 붙여서 차원 수가 그대로이고, stack은 새 차원을 만들어 쌓아서 차원이 하나 늘어요.
reshape()와 view()는 원소 수를 유지하면서 shape만 바꾼다. view()는 메모리 공유가 보장되지만 non-contiguous 텐서에는 사용할 수 없고, reshape()는 어디서나 사용할 수 있지만 메모리 공유를 보장하지 않는다. torch.cat()은 기존 차원을 따라 텐서를 이어 붙여 차원 수가 그대로이고, torch.stack()은 새 차원을 만들어 쌓으므로 차원 수가 하나 늘어난다.
12. 학습 데이터와 검증 데이터의 shuffle 설정이 다른 이유를 쓰고, 검증 단계에서 backward()와 step()을 하지 않는 이유를 설명하시오. (참고 문제)
학습 데이터는 epoch마다 순서를 섞어서 모델이 순서를 외우거나 한쪽에 치우치지 않게 해요(shuffle=True). 검증은 점수만 재니까 섞을 필요가 없어요(False). 검증 데이터로 backward와 step을 하면 시험지를 보고 공부한 셈이라 처음 보는 데이터에서의 실력을 공정하게 잴 수 없어서 안 해요.
학습 데이터는 epoch마다 데이터 순서를 섞어 batch 구성을 다양하게 해 순서나 한쪽 패턴에 치우치지 않도록 shuffle=True를 쓰고, 검증 데이터는 평가만 하므로 섞을 필요가 없어 shuffle=False를 쓴다. 검증 단계에서 backward()와 step()을 실행하면 모델이 검증 데이터까지 학습하게 되어 학습에 쓰지 않은 데이터에서의 성능을 공정하게 평가할 수 없으므로 하지 않는다.