NumPy와 PyTorch Tensor 연산 비교
PyTorch는 NumPy와 구조적으로 호환되는 다차원 텐서 연산 엔진을 제공한다. 기존 NumPy 배열을 PyTorch 텐서로 변환하거나 그 반대 과정은 데이터 전처리 및 추론 단계에서 빈번하게 활용된다.
import torch
import numpy as np
source_array = np.arange(6).reshape((2, 3))
converted_tensor = torch.from_numpy(source_array)
back_to_array = converted_tensor.numpy()
print("NumPy 원본:", source_array)
print("PyTorch 텐서:", converted_tensor)
print("변환 결과:", back_to_array)
기초 수학 연산은 NumPy와 유사한 인터페이스를 따르나, 행렬 곱셈(Matrix Multiplication) 시 주의가 필요하다. 2차원 행렬 연산은 NumPy의 np.matmul()과 대응되는 PyTorch 함수인 torch.mm() 또는 @ 연산자를 사용해야 한다. torch.dot()는 1차원 벡터 내적을 위한 함수이므로 2차원 배열에 적용 시 평탄화(Flatten)되어 예상치 못한 스칼라 값을 반환한다.
values = [-1.0, -2.0, 1.0, 2.0]
tensor_val = torch.FloatTensor(values)
print("절대값(Numpy):", np.abs(values))
print("절대값(PyTorch):", torch.abs(tensor_val))
print("사인(Numpy):", np.sin(values))
print("사인(PyTorch):", torch.sin(tensor_val))
matrix_a = torch.FloatTensor([[1, 2], [3, 4]])
print("행렬 곱(PyTorch mm):", torch.mm(matrix_a, matrix_a))
자동 미분(Autograd)과 계산 그래프
PyTorch의 핵심 기능 중 하나는 동적 계산 그래프를 통해 기울기(Gradient)를 자동으로 계산하는 autograd 모듈이다. 과거에는 Variable 클래스를 별도로 사용해야 했으나, 최신 버전에서는 텐서에 requires_grad=True 속성을 부여하는 것으로 대체되었다. 이 속성이 활성화되면 텐서 간의 모든 연산이 배경에서 계산 그래프에 기록되며, 최종 손실값에 대해 backward() 메서드를 호출할 때 연쇄 법칙을 적용하여 각 파라미터의 기울기를 역전파한다.
base_tensor = torch.FloatTensor([[1, 2], [3, 4]], requires_grad=True)
output_val = torch.mean(base_tensor ** 2)
print("정방향 연산 결과:", output_val.item())
output_val.backward()
print("미분 기울기 (d/dx):", base_tensor.grad)
정방향 연산 시 base_tensor의 각 원소가 제곱되어 평균을 구하면 7.5가 된다. 미분 식은 d(mean(x^2))/dx = 2x / 4 = x / 2이므로, 기울기 텐서는 원본 값의 절반인 [[0.5, 1.0], [1.5, 2.0]]으로 정확히 계산된다. 시각화 라이브러리와 연동해야 할 때는 .detach().numpy()를 활용하여 그래프에서 분리한 값만 전달해야 메모리 오류를 방지할 수 있다.
활성화 함수의 특성 분석
신경망이 비선형 관계를 학습하려면 활성화 함수가 필수적이다. ReLU는 계산 효율성과 기울기 소실 방지 측면에서 CNN의 표준으로 자리 잡았고, Sigmoid와 Tanh는 출력 범위 제한이나 순환 신경망에서 자주 사용된다. Softplus는 ReLU의 매끄러운 대체재로 활용된다.
import torch.nn.functional as F
import matplotlib.pyplot as plt
x_range = torch.linspace(-5, 5, 200)
activations = {
"ReLU": F.relu(x_range).numpy(),
"Sigmoid": F.sigmoid(x_range).numpy(),
"Tanh": F.tanh(x_range).numpy(),
"Softplus": F.softplus(x_range).numpy()
}
fig, axes = plt.subplots(2, 2, figsize=(8, 6))
for ax, (name, vals) in zip(axes.flatten(), activations.items()):
ax.plot(x_range.numpy(), vals, 'b-', label=name)
ax.set_title(name)
ax.grid(True)
ax.legend()
plt.tight_layout()
plt.show()
선형 회귀 모델 구현 및 학습 과정
연속형 변수 예측을 위한 회귀 작업은 torch.nn.Module을 상속하여 계층 구조를 정의한다. __init__ 메서드에서 레이어를 선언하고, forward 메서드에서 데이터의 정방향 흐름을 설계한다.
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
x_data = torch.unsqueeze(torch.linspace(-1, 1, 100), dim=1)
y_data = x_data ** 2 + 0.2 * torch.rand(x_data.size())
class RegressionNet(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.hidden_layer = nn.Linear(input_dim, hidden_dim)
self.output_layer = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
h = F.relu(self.hidden_layer(x))
return self.output_layer(h)
model = RegressionNet(1, 10, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
criterion = nn.MSELoss()
plt.ion()
plt.scatter(x_data.numpy(), y_data.numpy())
plt.pause(0.001)
for epoch in range(200):
pred = model(x_data)
loss = criterion(pred, y_data)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
plt.cla()
plt.scatter(x_data.numpy(), y_data.numpy(), s=10)
plt.plot(x_data.numpy(), pred.detach().numpy(), 'r-', lw=3)
plt.text(0.5, 0, f"Loss: {loss.item():.4f}", color='r', fontsize=15)
plt.pause(0.05)
다중 클래스 분류와 CrossEntropyLoss
범주형 데이터를 구분하는 분류 문제에서는 출력 차원을 클래스 수만큼 설정한다. PyTorch의 nn.CrossEntropyLoss는 내부적으로 Softmax와 Logarithm 연산을 포함하며, 정답 레이블은 원-핫(one-hot) 인코딩이 아닌 정수 클래스 인덱스(LongTensor) 형태로 제공해야 한다.
class0_x = torch.normal(mean=2, std=1, size=(100, 2))
class0_y = torch.zeros(100, dtype=torch.long)
class1_x = torch.normal(mean=-2, std=1, size=(100, 2))
class1_y = torch.ones(100, dtype=torch.long)
features = torch.cat((class0_x, class1_x), dim=0)
labels = torch.cat((class0_y, class1_y), dim=0)
class ClassifierNet(nn.Module):
def __init__(self, in_dim, mid_dim, out_dim):
super().__init__()
self.fc1 = nn.Linear(in_dim, mid_dim)
self.fc2 = nn.Linear(mid_dim, out_dim)
def forward(self, x):
return self.fc2(F.relu(self.fc1(x)))
classifier = ClassifierNet(2, 10, 2)
opt = torch.optim.Adam(classifier.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
for step in range(100):
logits = classifier(features)
loss = loss_fn(logits, labels)
opt.zero_grad()
loss.backward()
opt.step()
Sequential을 활용한 간결한 아키텍처 정의
데이터 흐름이 순차적으로만 진행되는 간단한 네트워크는 nn.Sequential 컨테이너를 사용하면 코드 가독성을 크게 높일 수 있다. 레이어를 순서대로 넘기면 forward 메서드 재정의 없이 자동으로 정방향 전파가 구성된다.
compact_net = nn.Sequential(
nn.Linear(1, 10),
nn.ReLU(),
nn.Linear(10, 1)
)
print("사용자 정의 클래스 방식:")
print(RegressionNet(1, 10, 1))
print("\nSequential 컨테이너 방식:")
print(compact_net)
학습 모델의 직렬화 및 복원
학습된 모델을 저장할 때는 전체 아키텍처를 포함한 상태를 저장하거나, 메모리 효율이 좋은 가중치 파라미터만 추출하여 저장할 수 있다. 복원 시에는 저장된 경로를 읽은 후 동일한 클래스 인스턴스에 load_state_dict() 메서드를 적용한다.
def train_and_save():
net = nn.Sequential(nn.Linear(1, 5), nn.ReLU(), nn.Linear(5, 1))
for _ in range(50):
pass
torch.save(net.state_dict(), 'model_weights.pt')
def load_and_infer():
restored_net = nn.Sequential(nn.Linear(1, 5), nn.ReLU(), nn.Linear(5, 1))
restored_net.load_state_dict(torch.load('model_weights.pt'))
restored_net.eval()
return restored_net
DataLoader를 통한 미니배치 처리
대용량 데이터를 일괄 처리하면 메모리 부족이 발생하므로, 데이터를 작은 단위(Mini-batch)로 분할하여 반복 학습하는 방식이 표준이다. torch.utils.data.DataLoader는 TensorDataset을 래핑하여 셔플, 다중 프로세스 로딩, 배치 크기 조절을 자동으로 처리한다.
import torch.utils.data as Data
sample_x = torch.linspace(1, 10, 10)
sample_y = torch.linspace(10, 1, 10)
dataset = Data.TensorDataset(sample_x, sample_y)
loader = Data.DataLoader(
dataset=dataset,
batch_size=4,
shuffle=True,
num_workers=2,
drop_last=False
)
for epoch in range(2):
for batch_idx, (bx, by) in enumerate(loader):
print(f"Epoch {epoch} | Batch {batch_idx} | X: {bx.tolist()} | Y: {by.tolist()}")
경사하강법 변형 및 최적화 알고리즘 평가
기본 SGD는 학습률이 고정되어 지역 최적점에 갇히기 쉽다. 이를 보완하기 위해 과거 업데이트 방향을 고려하는 Momentum, 학습률을 적응적으로 조정하는 RMSprop, 그리고 이들의 장점을 결합한 Adam 알고리즘이 제안되었다. 데이터 분포와 네트워크 깊이에 따라 최적의 성능을 내는 최적화기는 다르므로, 실험 단계에서 여러 알고리즘을 병렬로 비교하는 것이 중요하다.
optimizer_candidates = {
"SGD": torch.optim.SGD(model.parameters(), lr=0.01),
"Momentum": torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9),
"RMSprop": torch.optim.RMSprop(model.parameters(), lr=0.01, alpha=0.99),
"Adam": torch.optim.Adam(model.parameters(), lr=0.01)
}
for name, opt in optimizer_candidates.items():
pass