P2 실습에서는 Reuters 뉴스 기사를 이용하여 여러 클래스 중 하나를 예측하는 다중 클래스 분류를 다루었다.
이번 프로젝트에서는 새로운 텍스트 데이터인 20 Newsgroups를 이용하여 게시글의 주제를 분류한다.
프로젝트의 핵심은 Reuters 코드를 그대로 반복하는 것이 아니라,
새로운 다중 클래스 분류 문제에서 출력층·손실함수·평가지표를 선택하고, 일반화 성능과 오류를 판단하는 것
이다.
8.1프로젝트에서 해야 할 일¶
20개의 뉴스그룹 클래스와 데이터의 의미를 확인한다.
제공된 텍스트 벡터화 결과를 확인한다.
다중 클래스 분류에 적절한 모델을 설계한다.
Keras와 PyTorch에서 모두 기준 모델을 구성하고 훈련한다.
accuracy와 Top-3 accuracy를 함께 확인한다.
두 프레임워크에서 과대적합 여부와 일반화 성능을 비교한다.
각 프레임워크에서 일반화 성능을 개선하기 위한 방법을 하나 이상 적용한다.
Keras와 PyTorch의 최종 모델을 각각 테스트 데이터에서 평가한다.
어떤 클래스들이 서로 혼동되는지 분석한다.
8.2생성형 AI 활용¶
생성형 AI는 코드 작성·수정, 오류 메시지 해석, Keras와 PyTorch 코드 변환, 그래프 작성, 모델 개선 방법이나 오류 분석 아이디어 제안 등에 활용할 수 있다.
하지만 다음은 직접 확인하고 판단해야 한다.
출력 유닛 수는 몇 개여야 하는가?
어떤 손실함수와 평가지표가 적절한가?
accuracy와 Top-3 accuracy는 무엇을 다르게 보여주는가?
Keras와 PyTorch에서 같은 다중 클래스 분류 문제를 어떻게 표현하는가?
모델 크기는 적절한가?
과대적합이 나타나는가?
두 프레임워크의 결과를 어떻게 비교할 것인가?
어떤 클래스들이 왜 서로 혼동되는가?
프로젝트 마지막에 AI를 어디에 활용했고 무엇을 직접 검증했는지 기록한다.
8.3실행 환경 확인¶
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import tensorflow as tf
import keras
import torch
print("TensorFlow:", tf.__version__)
print("Keras:", keras.__version__)
print("PyTorch:", torch.__version__)
print("TensorFlow GPU:", tf.config.list_physical_devices("GPU"))
print("PyTorch CUDA:", torch.cuda.is_available())8.420 Newsgroups 데이터¶
20 Newsgroups는 게시글을 20개의 주제 중 하나로 분류하는 데이터셋이다.
Reuters와 마찬가지로 여러 클래스 중 하나를 예측하지만, 이번에는 다른 텍스트 데이터에서 같은 원리를 적용한다.
원시 텍스트의 복잡한 전처리가 프로젝트의 중심이 되지 않도록 데이터 불러오기와 기본 벡터화 코드는 제공한다.
from sklearn.datasets import fetch_20newsgroups
newsgroups_train = fetch_20newsgroups(
subset="train",
remove=("headers", "footers", "quotes"),
)
newsgroups_test = fetch_20newsgroups(
subset="test",
remove=("headers", "footers", "quotes"),
)
print("훈련 샘플 수:", len(newsgroups_train.data))
print("테스트 샘플 수:", len(newsgroups_test.data))
print("클래스 수:", len(newsgroups_train.target_names))8.4.1클래스 확인¶
for i, name in enumerate(newsgroups_train.target_names):
print(f"{i:2d}: {name}")8.4.2게시글 하나 확인¶
sample_index = 0
print("정답 클래스:", newsgroups_train.target[sample_index])
print("클래스 이름:", newsgroups_train.target_names[newsgroups_train.target[sample_index]])
print()
print(newsgroups_train.data[sample_index][:1500])8.4.3문제 1 — Reuters와 무엇이 같고 다른가?¶
이 문제는 왜 다중 클래스 분류인가?
한 게시글은 여러 주제에 동시에 속하는가, 하나의 주제에 속하는가?
출력층의 유닛 수는 몇 개가 되어야 하는가?
Reuters 문제와 공통점과 차이점을 설명하라.
답:
여기에 작성한다.
8.5훈련셋과 검증셋 분리¶
원래의 테스트 데이터는 최종 평가에만 사용한다.
훈련 데이터의 일부를 검증셋으로 분리하여 모델 구조와 일반화 성능을 판단한다.
클래스 비율이 크게 달라지지 않도록 stratify를 사용한다.
import numpy as np
from sklearn.model_selection import train_test_split
train_texts, val_texts, y_train, y_val = train_test_split(
newsgroups_train.data,
newsgroups_train.target,
test_size=0.2,
random_state=42,
stratify=newsgroups_train.target,
)
test_texts = newsgroups_test.data
y_test = np.asarray(newsgroups_test.target, dtype="int64")
y_train = np.asarray(y_train, dtype="int64")
y_val = np.asarray(y_val, dtype="int64")
print("train:", len(train_texts))
print("validation:", len(val_texts))
print("test:", len(test_texts))8.6텍스트 벡터화¶
신경망의 Dense 층은 문자열을 직접 입력으로 사용할 수 없다. 따라서 게시글을 고정 길이의 수치 벡터로 변환한다.
이번 프로젝트에서는 TF-IDF 벡터화를 제공한다.
프로젝트의 초점은 벡터화 방법 자체보다 다중 클래스 모델을 설계하고 평가하는 것이다.
중요한 점은 벡터화 기준도 훈련 데이터에서만 학습해야 한다는 것이다.
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(
max_features=3000,
stop_words="english",
dtype=np.float32,
)
X_train_sparse = vectorizer.fit_transform(train_texts)
X_val_sparse = vectorizer.transform(val_texts)
X_test_sparse = vectorizer.transform(test_texts)
X_train = X_train_sparse.toarray()
X_val = X_val_sparse.toarray()
X_test = X_test_sparse.toarray()
print("X_train:", X_train.shape)
print("X_val:", X_val.shape)
print("X_test:", X_test.shape)8.6.1벡터화 확인¶
한 게시글이 몇 개의 입력값으로 표현되는가?
모든 게시글의 원래 길이가 다른데도 Dense 층에 넣을 수 있는 이유는 무엇인가?
fit_transform()은 훈련 텍스트에만 사용하고 검증·테스트에는transform()만 사용하는 이유는 무엇인가?
답:
여기에 작성한다.
8.7모델링¶
이번 프로젝트에서는 Keras와 PyTorch를 모두 사용한다.
Reuters 실습의 모델은 좋은 출발점이지만, 이번에는 입력 차원과 클래스 수가 다르다.
가능하면 두 프레임워크에서 비슷한 은닉층 구조를 사용하여 같은 다중 클래스 분류 원리가 어떻게 표현되는지 비교한다.
다음 사항을 직접 결정한다.
은닉층 수
각 은닉층의 유닛 수
Dropout 또는 L2 규제 사용 여부
batch size
최대 epoch
조기 종료 사용 여부
8.7.1공통 모델 설계 원칙¶
Keras와 PyTorch에서 가능한 한 비슷한 구조를 사용한다.
은닉층 수:
각 은닉층의 유닛 수:
클래스 수:
출력층 유닛 수:
손실함수:
기본 평가지표:
추가 Top-k 지표:
batch size:
최대 epoch:
이 구조를 선택한 이유:
여기에 작성한다.
8.7.2Keras와 PyTorch에서 출력이 다른 점¶
같은 다중 클래스 분류 문제라도 구현 방식에는 차이가 있다.
Keras에서는 보통 마지막 층에
softmax를 두어 확률 분포를 출력한다.PyTorch에서는 보통 마지막 층에서 logits를 출력하고
CrossEntropyLoss()가 필요한 계산을 내부적으로 처리한다.
따라서 두 모델의 마지막 코드 모양은 다르지만 20개 클래스 중 하나를 예측한다는 문제 자체는 동일하다.
두 구현이 어떻게 대응되는지 프로젝트를 진행하면서 확인한다.
8.8.1Keras 기준 모델¶
from keras import layers
num_classes = len(newsgroups_train.target_names)
# TODO: 기준 모델을 구성한다.
keras_base = keras.Sequential([
# layers.Dense(..., activation="relu"),
# ...
# layers.Dense(num_classes, activation="softmax"),
])
# TODO: 모델 구조를 확인한다.
# keras_base.build(input_shape=(None, X_train.shape[1]))
# keras_base.summary()# TODO: Top-3 accuracy를 포함하여 compile하고 훈련한다.
#
# top_3_accuracy = keras.metrics.SparseTopKCategoricalAccuracy(
# k=3,
# name="top_3_accuracy"
# )
#
# keras_base.compile(
# optimizer="adam",
# loss="sparse_categorical_crossentropy",
# metrics=["accuracy", top_3_accuracy]
# )
#
# history_base = keras_base.fit(
# X_train,
# y_train,
# epochs=...,
# batch_size=...,
# validation_data=(X_val, y_val),
# verbose=1,
# )8.8.2Keras 개선 모델¶
기준 모델의 훈련·검증 결과를 확인한 뒤 다음 방법 중 하나 이상을 적용한다.
은닉층 크기 변경
L2 가중치 규제
Dropout
EarlyStopping
변경 내용과 이유:
여기에 작성한다.
# TODO: 개선 모델과 callback을 구성하여 훈련한다.
#
# improved_model = keras.Sequential([
# ...
# ])
#
# improved_model.compile(...)
#
# history_improved = improved_model.fit(
# X_train,
# y_train,
# validation_data=(X_val, y_val),
# ...
# )import torch
from torch import nn
from torch.utils.data import TensorDataset, DataLoader
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
train_dataset = TensorDataset(
torch.tensor(X_train, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.long)
)
val_dataset = TensorDataset(
torch.tensor(X_val, dtype=torch.float32),
torch.tensor(y_val, dtype=torch.long)
)
test_dataset = TensorDataset(
torch.tensor(X_test, dtype=torch.float32),
torch.tensor(y_test, dtype=torch.long)
)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=256, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False)
print("device:", device)8.9.1PyTorch 훈련 함수¶
PyTorch에서는 모델이 logits를 출력하고
CrossEntropyLoss()가 softmax에 해당하는 계산을 내부적으로 처리한다.
프로젝트의 중심이 반복문 작성이 되지 않도록 기본 훈련 함수를 제공한다.
def evaluate_multiclass(model, loader):
model.eval()
loss_fn = nn.CrossEntropyLoss()
total_loss = 0.0
correct = 0
top3_correct = 0
total = 0
with torch.no_grad():
for x_batch, y_batch in loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
logits = model(x_batch)
loss = loss_fn(logits, y_batch)
total_loss += loss.item() * len(x_batch)
correct += (logits.argmax(dim=1) == y_batch).sum().item()
top3 = logits.topk(3, dim=1).indices
top3_correct += (top3 == y_batch.unsqueeze(1)).any(dim=1).sum().item()
total += len(x_batch)
return {
"loss": total_loss / total,
"accuracy": correct / total,
"top_3_accuracy": top3_correct / total,
}
def train_multiclass_model(model, train_loader, val_loader, epochs=20,
weight_decay=0.0, patience=None):
model = model.to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(
model.parameters(),
weight_decay=weight_decay
)
history = {
"loss": [], "val_loss": [],
"accuracy": [], "val_accuracy": [],
"top_3_accuracy": [], "val_top_3_accuracy": []
}
best_val_loss = float("inf")
best_state = None
wait = 0
for epoch in range(epochs):
model.train()
total_loss = 0.0
correct = 0
top3_correct = 0
total = 0
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
optimizer.zero_grad()
logits = model(x_batch)
loss = loss_fn(logits, y_batch)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(x_batch)
correct += (logits.argmax(dim=1) == y_batch).sum().item()
top3 = logits.topk(3, dim=1).indices
top3_correct += (top3 == y_batch.unsqueeze(1)).any(dim=1).sum().item()
total += len(x_batch)
train_loss = total_loss / total
train_acc = correct / total
train_top3 = top3_correct / total
val_result = evaluate_multiclass(model, val_loader)
history["loss"].append(train_loss)
history["accuracy"].append(train_acc)
history["top_3_accuracy"].append(train_top3)
history["val_loss"].append(val_result["loss"])
history["val_accuracy"].append(val_result["accuracy"])
history["val_top_3_accuracy"].append(val_result["top_3_accuracy"])
if patience is not None:
if val_result["loss"] < best_val_loss:
best_val_loss = val_result["loss"]
best_state = {
k: v.detach().cpu().clone()
for k, v in model.state_dict().items()
}
wait = 0
else:
wait += 1
if wait >= patience:
break
if best_state is not None:
model.load_state_dict(best_state)
return model, history8.9.2PyTorch 기준 모델¶
num_classes = len(newsgroups_train.target_names)
# TODO: 기준 모델을 구성한다.
torch_base = nn.Sequential(
# nn.Linear(X_train.shape[1], ...),
# nn.ReLU(),
# ...
# nn.Linear(..., num_classes),
)
# TODO: 기준 모델을 훈련한다.
# torch_base, torch_history_base = train_multiclass_model(
# torch_base,
# train_loader,
# val_loader,
# epochs=...
# )# TODO: 개선 모델을 구성하고 훈련한다.
#
# torch_improved = nn.Sequential(
# ...
# )
#
# torch_improved, torch_history_improved = train_multiclass_model(
# torch_improved,
# train_loader,
# val_loader,
# epochs=...,
# weight_decay=...,
# patience=...
# )# Keras
keras_base_history = history_base.history
keras_improved_history = history_improved.history
# PyTorch
pytorch_base_history = torch_history_base
pytorch_improved_history = torch_history_improvedimport matplotlib.pyplot as plt
# TODO: 검증 loss를 비교한다.
#
# plt.figure(figsize=(7, 4))
# plt.plot(base_history["val_loss"], label="Base validation loss")
# plt.plot(improved_history["val_loss"], label="Improved validation loss")
# plt.xlabel("Epoch")
# plt.ylabel("Loss")
# plt.legend()
# plt.show()# TODO: validation accuracy와 validation top-3 accuracy를 확인한다.
#
# plt.figure(figsize=(7, 4))
# plt.plot(improved_history["val_accuracy"], label="Validation accuracy")
# plt.plot(
# improved_history["val_top_3_accuracy"],
# label="Validation top-3 accuracy"
# )
# plt.xlabel("Epoch")
# plt.ylabel("Accuracy")
# plt.legend()
# plt.show()8.10.1결과 비교와 모델 선택¶
다음 질문에 답한다.
Keras에서 훈련 loss와 검증 loss는 어떻게 변화했는가?
PyTorch에서 훈련 loss와 검증 loss는 어떻게 변화했는가?
각 프레임워크에서 과대적합이 나타났는가?
기준 모델과 개선 모델의 검증 accuracy는 어떻게 달라졌는가?
Top-3 accuracy는 일반 accuracy보다 왜 높은가?
Keras와 PyTorch의 검증 accuracy와 Top-3 accuracy는 얼마나 비슷하거나 달랐는가?
같은 구조를 사용했는데도 결과가 다르다면 가능한 이유는 무엇인가?
각 프레임워크에서 최종 모델로 어떤 모델을 선택하겠는가?
답:
여기에 비교와 판단을 작성한다.
# Keras 최종 모델
keras_final_model = ...
keras_results = keras_final_model.evaluate(
X_test, y_test, verbose=0
)
print("Keras test loss:", keras_results[0])
print("Keras test accuracy:", keras_results[1])
print("Keras test top-3 accuracy:", keras_results[2])# PyTorch 최종 모델
pytorch_final_model = ...
pytorch_test_result = evaluate_multiclass(
pytorch_final_model,
test_loader
)
print("PyTorch test result:", pytorch_test_result)# Keras 최종 모델의 테스트 예측
keras_probabilities = keras_final_model.predict(X_test, verbose=0)
keras_predictions = keras_probabilities.argmax(axis=1)
# PyTorch 최종 모델의 테스트 예측
pytorch_final_model.eval()
preds = []
with torch.no_grad():
for x_batch, _ in test_loader:
logits = pytorch_final_model(x_batch.to(device))
preds.append(logits.argmax(dim=1).cpu().numpy())
pytorch_predictions = np.concatenate(preds)# 두 프레임워크의 혼동행렬을 계산한다.
from sklearn.metrics import confusion_matrix
keras_cm = confusion_matrix(y_test, keras_predictions)
pytorch_cm = confusion_matrix(y_test, pytorch_predictions)
print("Keras confusion matrix:", keras_cm.shape)
print("PyTorch confusion matrix:", pytorch_cm.shape)8.12.1혼동되는 클래스 찾기¶
혼동행렬에서 대각선은 올바르게 분류한 샘플이고, 대각선 밖의 값은 다른 클래스로 잘못 분류한 샘플이다.
다음 질문에 답하라.
가장 많이 혼동되는 클래스 조합은 무엇인가?
두 클래스의 주제가 서로 비슷한가?
Top-3 예측을 함께 보면 오분류 사례를 다르게 해석할 수 있는가?
단순 accuracy만 확인했을 때 알 수 없었던 모델의 특성은 무엇인가?
답:
여기에 분석을 작성한다.
8.12.2오분류 게시글 직접 확인¶
오분류된 게시글 몇 개를 직접 읽어보고 정답 주제와 모델의 예측 주제를 비교한다.
# TODO: 오분류 인덱스를 찾고 일부 사례를 출력한다.
#
# wrong_indices = np.where(final_predictions != y_test)[0]
#
# for idx in wrong_indices[:5]:
# print("=" * 80)
# print("정답:", newsgroups_train.target_names[y_test[idx]])
# print("예측:", newsgroups_train.target_names[final_predictions[idx]])
# print()
# print(test_texts[idx][:1200])8.13프로젝트 최종 정리¶
8.13.1문제와 데이터¶
무엇을 예측했고 클래스는 몇 개인가?
8.13.2입력 표현¶
텍스트를 어떤 형태의 입력 벡터로 사용했는가?
8.13.3모델 설계¶
출력층, 손실함수, 평가지표를 어떻게 결정했는가?
8.13.4일반화¶
과대적합이 있었는가? 어떤 방법으로 대응했는가?
8.13.5Accuracy와 Top-3 accuracy¶
두 지표는 각각 무엇을 보여주었는가?
8.13.6최종 결과¶
테스트 accuracy와 Top-3 accuracy는 얼마인가?
8.13.7오류와 한계¶
어떤 클래스들이 서로 혼동되었고 그 이유는 무엇이라고 판단하는가?
8.13.8생성형 AI 활용과 검증¶
AI를 어디에 활용했고, 어떤 결과를 직접 확인·검증했는가?
8.14제출 전 확인¶
다중 클래스 분류 문제와 타깃의 의미를 설명했다.
20개 클래스의 의미를 확인했다.
텍스트 벡터화의 역할을 설명했다.
출력층과 손실함수·평가지표의 선택 이유를 설명했다.
Keras 기준 모델과 개선 모델을 모두 훈련했다.
PyTorch 기준 모델과 개선 모델을 모두 훈련했다.
두 프레임워크에서 훈련 성능과 검증 성능을 비교했다.
accuracy와 Top-3 accuracy를 함께 해석했다.
Keras와 PyTorch의 구현 방식과 결과를 비교했다.
각 프레임워크의 최종 모델을 선택하고 근거를 설명했다.
테스트셋은 각 최종 모델의 마지막 평가에만 사용했다.
두 프레임워크의 혼동행렬과 오분류 사례를 분석했다.
생성형 AI 활용 내용과 검증 방법을 기록했다.