실습 1: 신경망 입력을 위한 텐서 연습

오늘 할 일 — 75분

이론 1장의 사원 경력·연봉 표를 코드로 다룬다. 데이터 한 건과 변수 하나를 구분하고, 입력과 정답을 고르고, 주어진 예측의 오차를 계산한다. 마지막에는 신경망에서 사용할 행렬 곱을 연습한다.

대응 이론: Ch01 데이터와 모형. Python의 변수와 리스트를 배운 학생을 기준으로 하되, PyTorch의 새 문법은 사용 전에 설명한다.

  • Google Colab 기본 CPU 런타임을 사용한다. 학습 데이터를 내려받거나 GPU를 설정할 필요는 없다.
  • 각 구간의 시간에는 설명·따라 하기·직접 해보기가 포함되어 있다. 실습은 제출하거나 채점하지 않는다.
  • 직접 해보기에서는 먼저 출력을 예상하고, 작성 셀의 None을 바꾼다. 막히면 힌트를 본다.
  • 확인 셀은 제공 코드다. 작성 셀 다음에 실행하면 결과를 검사한다. assert는 조건이 맞는지 확인하고, 틀리면 실행을 멈춘다. 검사 코드를 작성하거나 외우는 것은 학습 목표가 아니다.
  • torch.equal은 값과 모양의 일치를, torch.allclose는 작은 소수 계산 오차를 허용한 값의 일치를 확인한다. 이들은 확인 셀에서 사용한다.
  • 해설은 문서 끝에 있다. 해설을 확인한 뒤에도 작성 셀로 돌아가 직접 실행한다.
시간 내용
0–12분 모듈·함수·속성 읽기, 텐서 생성
12–27분 데이터 표에서 행·열 고르기
27–37분 사칙연산과 오차
37–49분 합·평균으로 이론의 계산 확인
49–61분 행렬 곱
61–75분 종합 연습·설명·정리

1. 코드를 읽는 방법부터 — 12분

Colab에서 노트북을 본인 드라이브에 복사한다. Shift+Enter로 코드 셀을 실행한다. 셀은 위에서 아래로 실행한다. 런타임을 다시 시작하면 변수도 없어지므로 위에서부터 다시 실행한다.

import, 함수 호출, 결과 저장

모듈은 관련 기능을 모아둔 것이다. import torch는 PyTorch의 기능을 torch라는 이름으로 사용하게 한다. torch.tensor(...)는 괄호 안의 숫자 목록을 받아 텐서를 반환하는 함수다. 텐서는 숫자를 담고 계산하는 자료형이며, 한 줄짜리 벡터나 표 모양의 행렬을 표현할 수 있다.

문법 읽는 방법
torch.tensor(...) torch에 있는 tensor 함수를 호출한다
dtype=torch.float32 자료형을 지정하는 이름 붙은 인수다. 여기서는 실수 계산용 float32를 사용한다
v = ... 반환된 결과를 변수 v에 담는다
print(v) v의 내용을 화면에 출력한다
import torch

v = torch.tensor([1, 2, 3], dtype=torch.float32)
print(v)
tensor([1., 2., 3.])

속성은 읽고, 메서드는 실행한다

v.shape와 v.dtype은 텐서가 가진 속성이다. 이미 가진 정보를 읽으므로 괄호를 붙이지 않는다. 반면 뒤에서 쓸 v.sum()은 텐서의 합을 계산하는 메서드다. 텐서에 소속된 함수를 실행하므로 괄호를 붙인다. 지금은 속성 두 개부터 확인한다.

print(v.shape)     # 원소 3개: (3,)
print(v.dtype)     # torch.float32
torch.Size([3])
torch.float32

행렬은 바깥 리스트 안에 행별 리스트를 넣어 만든다. shape의 첫 숫자는 행 수, 둘째 숫자는 열 수다. 다음 결과를 먼저 예상한다.

table = torch.tensor([[1, 2],
                      [3, 4]], dtype=torch.float32)
print(table)
print(table.shape)
tensor([[1., 2.],
        [3., 4.]])
torch.Size([2, 2])

직접 해보기 — 텐서 만들기

숫자 [[1, 2], [3, 4], [5, 6]]을 담은 실수 텐서 practice를 만든다. 행과 열의 수를 먼저 말해 본다.

작성 셀

practice = None

힌트: torch.tensor(숫자 목록, dtype=torch.float32)를 사용한다.

확인 셀 — 작성 후 실행

assert isinstance(practice, torch.Tensor)
assert practice.shape == (3, 2) and practice.dtype == torch.float32
assert torch.equal(practice, torch.tensor([[1., 2.], [3., 4.], [5., 6.]]))
print('통과')

2. 이론의 데이터 표 고르기 — 15분

이론의 사원 8명 자료다. 연봉은 읽기 쉽도록 천 달러 단위로 썼다. 한 행이 사원 한 명, 한 열이 변수 하나다.

#                  경력(년)  연봉(천 달러)
staff = torch.tensor([[1.5, 31],
                      [2.5, 39],
                      [4.2, 43],
                      [5.1, 49],
                      [6.7, 54],
                      [8.3, 67],
                      [9.5, 92],
                      [13.0, 129]], dtype=torch.float32)

대괄호는 위치를 고른다

인덱스는 0부터 센다. staff[행, 열]로 위치를 고른다. a:b는 a번부터 b번 직전까지, :는 전부라는 뜻이다.

코드 고르는 것
staff[0, 1] 첫 사원의 연봉 한 값
staff[:3] 앞 세 사원의 전체 행
staff[:, 0] 모든 사원의 경력 값 목록
staff[:, 0:1] 경력 열 하나를 표 형태로 유지
print(staff[0, 1])
print(staff[:3])
print(staff[:, 0])
print(staff[:, 0:1])
tensor(31.)
tensor([[ 1.5000, 31.0000],
        [ 2.5000, 39.0000],
        [ 4.2000, 43.0000]])
tensor([ 1.5000,  2.5000,  4.2000,  5.1000,  6.7000,  8.3000,  9.5000, 13.0000])
tensor([[ 1.5000],
        [ 2.5000],
        [ 4.2000],
        [ 5.1000],
        [ 6.7000],
        [ 8.3000],
        [ 9.5000],
        [13.0000]])

이론에서 경력은 입력 \(X\), 연봉은 정답 \(y\)다. 이번 두 실습에서는 입력과 회귀 정답을 행이 데이터, 열이 변수인 2차원 표로 둔다. 0:1처럼 슬라이스를 쓰면 열 하나도 표로 유지된다. 한 건만 고를 때도 X[:1]로 행을 남긴다.

X = staff[:, 0:1]
y = staff[:, 1:2]
print(X.shape, y.shape)       # 둘 다 (8, 1)
print(X[:1])                 # 사원 한 명의 입력: (1, 1)
torch.Size([8, 1]) torch.Size([8, 1])
tensor([[1.5000]])

조건으로 행을 고른다

>=는 이상인지 비교한다. 열의 값들을 비교하면 사원마다 True 또는 False가 나온다. 이 결과를 조건 마스크라 하고 대괄호에 넣으면 True인 행만 남는다.

mask = staff[:, 0] >= 8
print(mask)
print(X[mask])
print(y[mask])
tensor([False, False, False, False, False,  True,  True,  True])
tensor([[ 8.3000],
        [ 9.5000],
        [13.0000]])
tensor([[ 67.],
        [ 92.],
        [129.]])

직접 해보기 — 입력과 정답을 함께 고르기

경력 5년 미만인 사원의 조건 junior_mask, 입력 junior_X, 정답 junior_y를 만든다.

작성 셀

junior_mask = None
junior_X = None
junior_y = None

힌트: 조건은 경력 열로 만들고, 같은 조건을 X와 y에 적용한다.

확인 셀 — 작성 후 실행

assert junior_mask is not None and junior_mask.tolist() == [True, True, True, False, False, False, False, False]
assert junior_X is not None and junior_X.shape == (3, 1)
assert torch.allclose(junior_X, torch.tensor([[1.5], [2.5], [4.2]]))
assert junior_y is not None and junior_y.shape == (3, 1)
assert torch.equal(junior_y, torch.tensor([[31.], [39.], [43.]]))
print('통과')

설명하기: 입력만 고르고 정답은 그대로 두면 어떤 문제가 생기는가?

3. 사칙연산으로 오차 계산 — 10분

+, -, *, /, ** 2는 각각 더하기, 빼기, 곱하기, 나누기, 제곱이다. 텐서에 숫자 하나를 곱하면 모든 원소에 같은 곱셈을 적용한다. 모양이 같은 두 텐서는 같은 위치끼리 계산한다.

print(y[:3] * 1000)     # 천 달러 → 달러
print(y[:3] / 12)       # 연봉 → 월급, 단위는 천 달러
tensor([[31000.],
        [39000.],
        [43000.]])
tensor([[2.5833],
        [3.2500],
        [3.5833]])

이론의 후보 모형 B는 연봉(천 달러) \(=10+8\times 경력\)이다. 첫 세 사원의 예측값은 22, 30, 43.6이다. 지금은 이 예측값을 주고 정답 − 예측을 계산한다. 예측을 만드는 신경망 부품은 다음 실습에서 배운다.

y_first = y[:3]
pred_B = torch.tensor([[22.], [30.], [43.6]])
error_B = y_first - pred_B
print(error_B)           # 약 [9, 9, -0.6]
print(error_B ** 2)
tensor([[ 9.0000],
        [ 9.0000],
        [-0.6000]])
tensor([[81.0000],
        [81.0000],
        [ 0.3600]])

직접 해보기 — 다른 후보의 오차 계산

이론의 후보 C는 앞 세 사원에게 25.5, 32.5, 44.4를 예측한다. pred_C, error_C, squared_C를 작성한다. 세 번째 사원의 오차 부호를 먼저 예상한다.

작성 셀

pred_C = None
error_C = None
squared_C = None

힌트: 예측 텐서는 정답과 같은 (3, 1)로 만든다.

확인 셀 — 작성 후 실행

assert pred_C is not None and pred_C.shape == y_first.shape
assert torch.allclose(pred_C, torch.tensor([[25.5], [32.5], [44.4]]))
assert error_C is not None and torch.allclose(error_C, torch.tensor([[5.5], [6.5], [-1.4]]), atol=1e-5)
assert squared_C is not None and torch.allclose(squared_C, torch.tensor([[30.25], [42.25], [1.96]]), atol=1e-4)
print('통과')

설명하기: 오차가 음수이면 예측이 정답보다 큰가, 작은가?

4. 합과 평균으로 비교하기 — 12분

sum(), mean(), item()

메서드 하는 일 반환값
a.sum() 전체 원소를 더한다 원소 하나인 텐서
a.mean() 전체 원소의 평균을 구한다 원소 하나인 텐서
a.item() 원소 하나인 텐서에서 숫자를 꺼낸다 Python 숫자

이론의 잔차 제곱합은 오차를 제곱해서 더한 값이다. 작을수록 주어진 데이터를 더 가깝게 예측한다.

sse_B = (error_B ** 2).sum()
print(sse_B.item())        # 약 162.36: 앞 세 사원에 대한 값
print(y_first.mean())
162.36000061035156
tensor(37.6667)

dim은 합치려는 축을 지정한다

2차원 표에서 0번 축은 행, 1번 축은 열이다. dim=0은 여러 행을 합쳐 열별 결과, dim=1은 여러 열을 합쳐 행별 결과를 만든다. 인수를 생략하면 전체를 합친다.

M = torch.tensor([[1., 2., 3.],
                  [4., 5., 6.]])
print(M.sum(dim=0))     # [5, 7, 9]
print(M.sum(dim=1))     # [6, 15]
print(M.mean(dim=0))    # [2.5, 3.5, 4.5]
tensor([5., 7., 9.])
tensor([ 6., 15.])
tensor([2.5000, 3.5000, 4.5000])
all_B = torch.tensor([[22.], [30.], [43.6], [50.8], [63.6], [76.4], [86.], [114.]])
all_C = torch.tensor([[25.5], [32.5], [44.4], [50.7], [61.9], [73.1], [81.5], [106.]])

직접 해보기 — 같은 데이터에서 후보 비교

아래 두 예측은 사원 8명 전체에 대한 것이다. 각각의 잔차 제곱합 sse_all_B, sse_all_C와 사원 표의 열별 평균 column_mean을 구한다.

작성 셀

sse_all_B = None
sse_all_C = None
column_mean = None

힌트: 두 후보를 같은 8명의 정답과 비교한다. 열별 평균은 dim=0이다.

확인 셀 — 작성 후 실행

assert sse_all_B is not None and abs(sse_all_B.item() - 607.12) < 0.02
assert sse_all_C is not None and abs(sse_all_C.item() - 816.22) < 0.02
assert column_mean is not None and column_mean.shape == (2,)
assert torch.allclose(column_mean, torch.tensor([6.35, 63.]))
print('통과')

설명하기: 어느 후보가 더 나은가? 앞 세 사원만 비교했을 때와 결론이 같은가?

5. 행렬 곱을 읽고 쓰기 — 12분

A @ B는 왼쪽의 행과 오른쪽의 열을 곱해서 더하는 연산이다. *는 같은 위치끼리 곱하기만 한다. 두 결과는 다르다.

\[(n,p)\ @\ (p,q)\longrightarrow(n,q)\]

왼쪽의 열 수와 오른쪽의 행 수가 같아야 한다. 결과는 왼쪽의 행 수와 오른쪽의 열 수를 갖는다. 아래 A @ B의 첫 값은 \(1\times2+2\times1=4\)다. 나머지 값도 먼저 예상한다.

A = torch.tensor([[1., 2.], [3., 4.]])
B = torch.tensor([[2., 0.], [1., 3.]])
print(A * B)
print(A @ B)
print(A[:1] @ B)       # 한 행만 넣어도 계산 순서는 같다
tensor([[ 2.,  0.],
        [ 3., 12.]])
tensor([[ 4.,  6.],
        [10., 12.]])
tensor([[4., 6.]])
data = torch.tensor([[1., 2.], [3., 4.], [5., 6.]])

직접 해보기 — 행렬 곱으로 두 출력을 만들기

입력 data의 각 행을 B와 곱한 결과를 result에 담는다. 첫 행만 계산한 결과는 one_result에 담는다. 두 결과의 모양을 먼저 예상한다.

작성 셀

result = None
one_result = None

힌트: 데이터를 왼쪽, 계산에 쓰는 행렬을 오른쪽에 둔다.

확인 셀 — 작성 후 실행

assert result is not None and result.shape == (3, 2)
assert torch.allclose(result, torch.tensor([[4., 6.], [10., 12.], [16., 18.]]))
assert one_result is not None and one_result.shape == (1, 2)
assert torch.allclose(one_result, result[:1])
print('통과')

6. 종합 연습 — 14분

새 카페 표에서도 같은 문법을 쓸 수 있는지 확인한다. 9분 동안 작성하고, 남은 시간에 결과를 함께 설명한다.

#              방문자(십 명) 광고비(만 원) 매출(만 원)
cafe = torch.tensor([[1., 0., 6.],
                     [2., 1., 12.],
                     [3., 0., 14.],
                     [4., 2., 24.]])
cafe_pred = torch.tensor([[6.], [13.], [14.], [24.]])

직접 해보기 — 표에서 입력·정답·오차까지

  1. 앞 두 열을 입력 cafe_X, 마지막 열을 정답 cafe_y로 고른다. 둘 다 표 형태로 둔다.
  2. 입력의 열별 평균 feature_mean을 구한다.
  3. 예측 cafe_pred의 잔차 제곱합 cafe_sse를 구한다.
  4. 광고비가 0인 날짜의 실제 매출 평균 no_ad_mean을 구한다.

작성 셀

cafe_X = None
cafe_y = None
feature_mean = None
cafe_sse = None
no_ad_mean = None

힌트: 행·열 선택은 2절, 합과 평균은 4절로 돌아가 확인한다.

확인 셀 — 작성 후 실행

assert cafe_X is not None and cafe_X.shape == (4, 2) and torch.equal(cafe_X, cafe[:, :2])
assert cafe_y is not None and cafe_y.shape == (4, 1) and torch.equal(cafe_y, cafe[:, 2:3])
assert feature_mean is not None and feature_mean.shape == (2,)
assert torch.allclose(feature_mean, torch.tensor([2.5, 0.75]))
assert cafe_sse is not None and abs(cafe_sse.item() - 1.) < 1e-5
assert no_ad_mean is not None and abs(no_ad_mean.item() - 10.) < 1e-5
print('통과')

설명하기: 매출 열을 입력에 넣으면 왜 안 되는가? 먼저 끝났다면 특정 날짜의 예측을 바꾸고 잔차 제곱합이 커지는지 확인한다.

마무리

오늘 익힌 순서는 모듈 가져오기 → 텐서 만들기 → 행·열 선택 → 연산 → 결과 확인이다. shape처럼 정보를 읽는 속성과 sum()처럼 계산하는 메서드를 구분한다.

실습 2에서는 준비한 표를 신경망 부품에 넣어 예측을 만든다.


해설 — 먼저 직접 풀고 확인하기

해설 코드를 해당 작성 셀에 옮긴 후 확인 셀을 실행한다.

텐서 만들기

practice = torch.tensor([[1, 2], [3, 4], [5, 6]], dtype=torch.float32)

입력과 정답을 함께 고르기

junior_mask = staff[:, 0] < 5
junior_X = X[junior_mask]
junior_y = y[junior_mask]

다른 후보의 오차 계산

pred_C = torch.tensor([[25.5], [32.5], [44.4]])
error_C = y_first - pred_C
squared_C = error_C ** 2

같은 데이터에서 후보 비교

sse_all_B = ((y - all_B) ** 2).sum()
sse_all_C = ((y - all_C) ** 2).sum()
column_mean = staff.mean(dim=0)

행렬 곱으로 두 출력을 만들기

result = data @ B
one_result = data[:1] @ B

표에서 입력·정답·오차까지

cafe_X = cafe[:, :2]
cafe_y = cafe[:, 2:3]
feature_mean = cafe_X.mean(dim=0)
cafe_sse = ((cafe_y - cafe_pred) ** 2).sum()
no_ad_mean = cafe[cafe[:, 1] == 0, 2].mean()

후보 비교 해석: 앞 세 사원에서는 C의 잔차 제곱합이 약 74.46으로 B의 162.36보다 작다. 8명 전체에서는 B가 약 607.12, C가 약 816.22로 B가 더 작다. 비교에 사용한 데이터 범위를 함께 봐야 한다.