오늘은 MNIST 이미지 처리와 퍼셉트론에 대해 배웠다. 복잡한 코드를 많이 배우니 따라가기도 벅찬 하루였다. 그래도 이전에 한 번 배웠던 내용이라 괜찮을 줄 알았는데 이전에 맛보기로 배웠던 내용들을 본격적으로 배우려니 생각보다 어려웠던 것 같다.
| 구분 | TensorFlow | PyTorch |
|---|---|---|
| 데이터 로드 API | tf.keras.datasets.mnist.load_data() | torchvision.datasets.MNIST |
| 데이터 로딩 방식 | NumPy 배열 기반 | Dataset + DataLoader |
| 반환 자료형 | numpy.ndarray | torch.Tensor |
| 이미지 형태 | (H, W) | (C, H, W) |
| 이미지 크기 | ||
| 색상 채널 차원 | 기본적으로 없음 | 기본적으로 포함 () |
| CNN 입력용 형태 | (28, 28, 1)로 직접 차원을 추가해야 함 | (1, 28, 28) |
| 픽셀 값 범위 | 0 ~ 255 (uint8) | 0 ~ 255 (uint8) |
| 정규화 | 사용자가 직접 255로 나눠서 정규화 | transforms.ToTensor()에서 자동 수행 |
import torch
from torchvision.datasets import MNIST
from torchvision import transforms
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device)
train_mnist = MNIST(
root=DIR_PATH,
train=True,
transform=None,
download=True
)
type(train_mnist)
# torchvision.datasets.mnist.MNIST
len(train_mnist)
# 60000
train_mnist[0]
# (<PIL.Image.Image image mode=L size=28x28>, 5)
train_mnist.data
type(train_mnist)
# torchvision.datasets.mnist.MNIST
train_mnist.data.shape
# torch.Size([60000, 28, 28])
train_mnist.data.dtype
# torch.uint8
img = train_mnist.data[0]
type(img)
# torch.Tensor
img.shape
# torch.Size([28, 28])
train_mnist.targets
# tensor([5, 0, 4, ..., 5, 6, 8])
train_mnist.targets[0]
# tensor(5)
train_mnist.classes
# ['0 - zero',
# '1 - one',
# '2 - two',
# '3 - three',
# '4 - four',
# '5 - five',
# '6 - six',
# '7 - seven',
# '8 - eight',
# '9 - nine']
train_mnist.class_to_idx
# {'0 - zero': 0,
# '1 - one': 1,
# '2 - two': 2,
# '3 - three': 3,
# '4 - four': 4,
# '5 - five': 5,
# '6 - six': 6,
# '7 - seven': 7,
# '8 - eight': 8,
# '9 - nine': 9}
import matplotlib.pyplot as plt
for i in range(9):
plt.subplot(3, 3, i + 1)
img = train_mnist.data[i]
plt.imshow(X=img, cmap='gray')
plt.title(label=f'label : {train_mnist.targets[i]}')
plt.axis('off')

pixels = train_mnist.data.float() / 255
pixels.shape
# torch.Size([60000, 28, 28])
pixels.mean() # tensor(0.1307)
pixels.std() # tensor(0.3081)
mnist_avg = round(pixels.mean().item(), 3)
mnist_std = round(pixels.std().item(), 3)
transform = transforms.Compose(
transforms=[
transforms.ToTensor(),
transforms.Normalize(mean=mnist_avg, std=mnist_std)
]
)
train_mnist = MNIST(
root=DIR_PATH,
train=True,
transform=transform
)
img, label = train_mnist[0]
type(img)
# torch.Tensor
img.shape
# torch.Size([1, 28, 28])
plt.imshow(X=img.squeeze(dim=0), cmap='gray')
plt.axis('off');

test_mnist = MNIST(
root=DIR_PATH,
train=False,
transform=transform
)
| 구분 | 활성화 함수 | 이진 분류 문제 | 다중 분류 문제 |
|---|---|---|---|
| 활성화 함수 | 항등 함수 | 시그모이드 함수 | 소프트맥스 함수 |
| 구분 | 상세 내용 |
|---|---|
| SGD | • Stochastic Gradient Descent의 줄임말 • 가장 기본적인 최적화 알고리즘으로, 현재 기울기만을 사용하여 가중치를 갱신 • 구조가 단순하여 이해하기 쉽지만, 수렴 속도가 느리고 학습이 불안정할 수 있음 |
| SGD with Momentum | • 이전 기울기의 방향성을 함께 고려하여 가중치를 갱신하는 방식 • 진동을 줄이고 수렴 속도를 개선할 수 있어, 기본 SGD보다 안정적인 학습이 가능 |
| Adam | • Adaptive Moment Estimation의 줄임말 • Momentum과 적응성 학습률 조정을 결합한 최적화 알고리즘 • 학습률 튜닝이 비교적 쉽고 빠르게 수렴하는 특징이 있어, 전이학습에서 널리 사용 |
| AdamW | • Adam에서 가중치 감소(Weight Decay)를 분리하여 적용한 방식 • 일반화 성능이 개선되는 경우가 많아, 최근 실무에서 자주 사용 |
img1 = train_mnist[0][0]
img2 = train_mnist[1][0]
print(img1.shape)
print(img2.shape)
# torch.Size([1, 28, 28])
# torch.Size([1, 28, 28])
stacked_images = torch.stack(tensors=[img1, img2], dim=0)
stacked_images.shape
# torch.Size([2, 1, 28, 28])
flattened_image = img1.flatten()
flattened_image.shape
# torch.Size([784])
train_images = torch.stack(tensors=[img for img, _ in train_mnist], dim=0)
train_labels = torch.tensor(data=[label for _, label in train_mnist])
train_images.shape
# torch.Size([60000, 1, 28, 28])
train_labels.shape
# torch.Size([60000])
train_images = train_images.reshape(train_images.shape[0], -1)
train_images.shape
# torch.Size([60000, 784])
test_images = torch.stack(tensors=[img for img, _ in test_mnist])
test_labels = torch.tensor(data=[label for _, label in test_mnist])
test_images = test_images.reshape(test_images.shape[0], -1)
test_images.shape
# torch.Size([10000, 784])
import random
def set_seed(seed=0):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
deterministic True 지정 시 일부 연산이 느려질 수 있고, 에러 발생 가능import random
def set_seed_gpu(seed=0, deterministic=False):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
if deterministic:
torch.backends.cudnn.deterministic=True
torch.backends.cudnn.benchmark=False
torch.use_deterministic_algorithms(True)
import torch.nn as nn
model = nn.Linear(in_features=784, out_features=10).to(device)
model.weight.shape
# torch.Size([10, 784])
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.001)
model.train()
logits = model(train_images)
loss = criterion(logits, train_labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad() 설정with torch.no_grad():
logits = model(test_images) # 클래스별 예측 점수 계산
y_prob = torch.softmax(input=logits, dim=1) # 클래스별 예측 확률 계산
y_pred = y_prob.argmax(dim=1) # 가장 큰 예측 확률을 갖는 클래스를 예측값으로 선택
acc = (test_labels == y_pred).float().mean().item() # 실제값과 예측값의 일치 정도 계산
print(acc)
@torch.no_grad()@torch.no_grad()
def accuracy(model, x, y):
model.eval()
logits = model(x)
y_prob = torch.softmax(input=logits, dim=1) # 생략 가능
y_pred = y_prob.argmax(dim=1)
acc = (y == y_pred).float().mean().item()
return acc
def train(model, x, y):
logits = model(x)
loss = criterion(logits, y)
optimizer.zero_grad()
loss.backwar()
optimizer.step()
return loss.item()
train_losses = []
train_accs = []
test_accs = []
for epoch in range(20):
model.train()
loss = train(model, train_images, train_labels)
train_losses.append(loss)
train_acc = accuracy(model, train_images, train_labels)
test_acc = accuracy(model, test_images, test_labels)
train_accs.append(train_acc)
test_accs.append(test_acc)
print(f'[Epoch : {epoch + 1:02d}] Loss : {loss:.4f} ',
f'Train_Acc = {train_acc:.4f}, Test_ACC = {test_acc:.4f}')
plt.plot(train_losses, label='훈련셋 손실값')
plt.title('훈련셋 손실값 변화')
plt.xlabel('에포크')
plt.ylabel('손실값')
plt.legend()
plt.show()

plt.plot(train_accs, label='훈련셋 정확도')
plt.plot(test_accs, label='시험셋 정확도')
plt.title('정확도 변화')
plt.xlabel('에포크')
plt.ylabel('정확도')
plt.legend()
plt.show()

optimizer.zero_grad()가 필요한 이유loss.backward() 를 호출할 때 기울기를 자동으로 누적하는 방식으로 동작optimizer.zero_grad() 를 호출하지 않으면 의도하지 않은 파라미터 업데이트가 발생optimizer.zero_grad() 는 이전에 계산된 기울기를 모두 초기화torch.save(obj=model.state_dict(), f=SAVE_PATH)
model_loaded = nn.Linear(in_features=784, out_features=10)
model_params = torch.load(f=SAVE_PATH, map_location=device)
model_loaded.load_state_dict(model_params)
model.eval()
with torch.no_grad():
test_logits = model(test_images)
test_probs = torch.softmax(input=test_logits, dim=1)
test_preds = test_probs.argmax(dim=1)
y_pred = pd.Series(data=test_preds, name='y_pred')
y_true = pd.Series(data=test_labels, name='y_true')
cfmx = pd.crosstab(index=y_true, columns=y_pred)
cfmx.index = train_mnist.classes
cfmx.columns = train_mnist.classes
plt.figure(figsize=(8, 6), dpi=120)
sns.heatmap(data=cfmx, cmap='viridis_r', annot=True, annot_kws={'size': 8})
plt.show()

n = 10
mis_index = np.where(y_true.ne(y_pred))[0][:n]
fig, axes = plt.subplots(2, 5, figsize=(8, 4), dpi=120)
for ax, i in zip(axes.flatten(), mis_index):
ax.imshow(X=test_images[i].reshape(28, 28), cmap='gray')
ax.set_title(label=f'[{i.item()}] True : {y_true[i]} Pred : {y_pred[i]}', size=8)
ax.axis('off')
cond = y_true.eq(2) & y_pred.eq(8)
mis_index = np.where(cond)[0]
fig, axes = plt.subplots(2, 5, figsize=(8, 4), dpi=120)
for ax, i in zip(axes.flatten(), mis_index):
ax.imshow(X=test_images[i].reshape(28, 28), cmap='gray')
ax.set_title(label=f'[{i.item()}] True : {y_true[i]} Pred : {y_pred[i]}', size=8)
ax.axis('off')
from PIL import Image
test_img = Image.open(fp='mnist_test.png', mode='r')
test_img.size
test_img = test_img.resize((28, 28))
test_img.size
# (28, 28)
test_img.mode
# 'RGBA'
test_img = test_img.convert(mode='L')
test_img = np.array(test_img)
test_img = np.invert(test_img)
import cv2
threshold, _ = cv2.threshold(src=test_img, thresh=0, maxval=255, type=cv2.THRESH_BINARY + cv2.THRESH_OTSU)
test_img = np.where(test_img < threshold, 0, test_img)
test_img = torch.tensor(test_img, dtype=torch.float32)
test_img = test_img.unsqueeze(dim=0)
test_img = test_img.flatten(1, -1)
test_img.shape
# torch.Size([1, 784])
model.eval()
logits = model(test_img)
y_prob = torch.softmax(input=logits, dim=1)
y_pred = y_prob.argmax(dim=1)
print(y_pred)
def guess_digit(file):
img = Image.open(fp=file, mode='r')
img = img.resize((28, 28)).convert(mode='L')
img = np.invert(np.array(img))
thresh, _ = cv2.threshold(src=img, thresh=0, maxval=255, type=cv2.THRESH_BINARY + cv2.THRESH_OTSU)
img = np.where(img < thresh, 0, img)
img = torch.tensor(img, dtype=torch.float32)
img = img.unsqueeze(dim=0).flatten(1, -1)
logits = model(img)
y_pred = logits.argmax(dim=1)
return y_pred
이번 주말에는 일정이 있지만 시간 남을 때 도메인을 하나 정해서 개인 프로젝트를 해볼 예정이다. 최근 공고를 봤을 때 자주 보이던 A/B 테스트 분석을 먼저 시작해볼까 싶은데 어떤 프로젝트인지와 어떤 데이터가 필요한지를 찾아봐야겠다.