-
[DL] 차 상태 예측 분류 신경망 모델 구축 실습ML,DL 2024. 3. 22. 13:19
자동차 평가 데이터셋을 사용하여, 자동차의 6개 피처 특성을 바탕으로 7번째 피처인 '차 상태' 를 예측하는 분류 모델이다.
사용된 신경망 모델은 임베딩 레이어를 사용하여
범주형 입력 데이터의 차원 축소 → 선형 레이어 → ReLU → 배치 정규화 → 드롭 아웃 을 통과하여
최종 출력은 “차 상태”를 분류하는 4개의 클래스에 해당하는 확률을 나타낸다.
모델은 주어진 epoch동안 학습되며, 각 epoch마다 모델의 출력인 예측값과 실제 레이블 간 차이인 loss를 계산하고 이를 기반으로 모델 weight를 업데이트한다.
학습 과정에서 loss가 감소하는 것을 목표로 한다. Loss로 CrossEntropy를 사용하였고, 최적화를 위해 optimizer로 Adam을 사용하였다.
프레임워크로는 파이토치를 사용하였다. 파이토치에서는 가장 기본적인 데이터 구조로 텐서를 사용한다. 그렇기 때문에, 텐서 형태로 데이터를 변환하는 과정을 먼저 하고 나서, 모델 학습에 훈련 데이터로 사용하였다.
#벡터, 행렬, 텐서
인공지능 (ML/DL)에서 데이터는 벡터로 표현된다. 벡터는 [1,2,3]처럼 숫자들이 리스트로 1차원(axis=0) 배열 형태이다.
matrix는 행과 열로 표현되는 2차원(axis=2) 배열 형태이다. 가로줄을 rwo, 세로줄을 column이라고 한다.
텐서는 3차원(axis=3) 이상의 배열 형태이다.
데이터분석
데이터 셋인 car_evaluation.csv 파일은 7개의 피처로 구성되어 있다.
- price : 자동차 가격
- maint : 자동차 유지 비용
- doors : 자동차 문 개수
- persons : 수용 인원
- lug_capacity : 수하물 용량
- safety : 안전성
- output : 차 상태, 이 데이터는 unacc(허용 불가능한 수준) 및 acc(허용 가능한 수준), 양호(good) 및 매우 좋은(verygood, vgood) 중 하나의 값을 갖는다.
라이브러리 설치
matplotlib : 2D, 3D 형태의 그래프(플롯)를 그릴 때 주로 사용
seaborn : 데이터 프레임으로 다양한 통계 지표 표현할 수 있는 시각화 차트 제공, 데이터 분석에 많이 쓰임
sckit-learn : classification / regression / clustering / decision tree 등 다양한 머신 러닝 알고리즘을 적용할 수 있는 함수를 제공하는 머신 러닝 라이브러리
라이브러리 로드
import torch import torch.nn as nn import numpy as np # 벡터 및 메트릭스 연산 import pandas as pd # 데이터 처리 import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline
데이터 로드 & 데이터 프레임 내 처음 5개 데이터 출력하기
dataset = pd.read_csv('/content/drive/MyDrive/pytorch_practice/car_evaluation.csv') dataset.head()
EDA
데이터 분포 시각화
#output 피처의 데이터셋 분포 확인하기 fig_size = plt.rcParams["figure.figsize"] fig_size[0] = 8 fig_size[1] = 6 plt.rcParams["figure.figsize"] = fig_size dataset.output.value_counts().plot(kind='pie', autopct='%0.05f%%', colors=['lightblue','lightgreen','orange','pink'], explode=(0.05,0.05,0.05,0.05))
분석 결과
⇒ 자동차 70%는 차의 상태가 unacc(허용 불가능한 상태)이다. 20%만 허용 가능한 수준이다. 즉, 양호한 자동차 비율이 매우 낮은 것을 알 수 있다.
데이터전처리
딥러닝은 통계 알고리즘을 기반으로 하기 때문에 단어 → 숫자(텐서) 로 변환해야 한다.
가장 먼저 필요한 전처리는 데이터를 파악하는 것이다. 예제에서 다루는 피처들은 모두 범주형 데이터이다.
범주형 데이터로 변환 + Numpy Array로 변환
astype()을 이용해 범주 특성을 갖는 데이터를 범주형 타입(category)으로 변환한다.
- object유형은 각 값이 개별적으로 저장되어 많은 메모리를 소비하지만, 카테고리유형은 데이터를 고유한 카테고리로 저장하고 각 인스턴스를 코드에 매핑하여 총 인스턴스 수에 비해 고유 범주 수가 적은 경우 메모리 사용공간을 크게 줄일 수 있다.
cat.codes : 범주형 데이터를 숫자로 변환
np.stack : 두 개 이상의 넘파이 객체를 합칠 때 사용한다. 서로 다른 두 배열을 새로운 축으로 합쳐준다. 만얄 1차원 배열들인 경우 합쳐서 2차원배열로 만든다. 따라서 반드시 두 배열의 차원이 동일해야 한다.
# 범주형 데이터 셋의 피처 목록 categorical_columns = ['price','maint','doors','persons','lug_capacity','safety'] # 범주형 특성을 갖는 피처들을 범주형 타입으로 변환 (object타입 -> category타입) for category in categorical_columns: dataset[category] = dataset[category].astype('category') #각 범주형 피처에 대한 범주 데이터를 각각 다른 숫자 코드로 변환 price = dataset['price'].cat.codes.values maint = dataset['maint'].cat.codes.values doors = dataset['doors'].cat.codes.values persons = dataset['persons'].cat.codes.values lug_capacity = dataset['lug_capacity'].cat.codes.values safety = dataset['safety'].cat.codes.values #범주형 데이터 -> Numpy Array형태로 변환 categorical_data = np.stack([price, maint, doors, persons, lug_capacity, safety], 1) #변환된 처음 10개의 데이터 표시 categorical_data[:10] """ array([[3, 3, 0, 0, 2, 1], [3, 3, 0, 0, 2, 2], [3, 3, 0, 0, 2, 0], [3, 3, 0, 0, 1, 1], [3, 3, 0, 0, 1, 2], [3, 3, 0, 0, 1, 0], [3, 3, 0, 0, 0, 1], [3, 3, 0, 0, 0, 2], [3, 3, 0, 0, 0, 0], [3, 3, 0, 1, 2, 1]], dtype=int8) """
텐서로 변환
범주형 데이터 → dataset[category] → Numpy array → Tensor
파이토치로 모델을 학습시키기 위해서는 텐서 형태로 변환해야 하는데, Numpy array를 통해 텐서를 생성할 수 있다.
categorical_data = torch.tensor(categorical_data, dtype=torch.int64) categorical_data[:10] """ tensor([[3, 3, 0, 0, 2, 1], [3, 3, 0, 0, 2, 2], [3, 3, 0, 0, 2, 0], [3, 3, 0, 0, 1, 1], [3, 3, 0, 0, 1, 2], [3, 3, 0, 0, 1, 0], [3, 3, 0, 0, 0, 1], [3, 3, 0, 0, 0, 2], [3, 3, 0, 0, 0, 0], [3, 3, 0, 1, 2, 1]])"""
레이블로 사용할 피처인 outputs도 텐서로 변환해준다. (범주형 → 넘파이배열 → 텐서)
outputs = pd.get_dummies(dataset.output) #범주형 => numpy array (원-핫 인코딩) outputs = outputs.values outputs = torch.tensor(outputs).flatten() # numpy array -> 1차원 tensor print(categorical_data.shape) print(outputs.shape) """ torch.Size([1728, 6]) torch.Size([6912])"""
- get_dummies : dummy variable로 만들어 주는 함수이다. 다시말해, 문자를 숫자(0,1)로 바꾼다. 원래 숫자인 데이터는 그 데이터가 그대로 유지되고, 숫자가 아닌 범주형 데이터는 0또는 1로 변환된다. (원 - 핫 인코딩)
- ravel(), reshape(), flatten() 은 텐서의 차원을 바꿀 때 사용한다.
워드 임베딩을 위해, 범주형 피처를 N차원으로 변환
워드 임베딩은 유사한 단어끼리 유사하게 인코딩되도록 표현하는 방법이다.
높은 차원의 임베딩일수록 단어 간의 세부적인 관계를 잘 파악할 수 있다. 따라서 단일 숫자로 변환된 numpy array를 N차원으로 변경한다.
- 배열을 N차원으로 변환하기 위해 먼저 모든 범주형 피처에 대한 임베딩 크기(벡터 차원 크기)를 정의한다.
- 임베딩 크기에 대한 정확한 규칙은 없지만, 피처의 고유 값 수를 2로 나누는 것을 많이 사용한다. 예를 들어 price 피처는 4개의 고유 값을 갖기 때문에 임베딩 크기는 4/2=2 이다.
categorical_column_sizes = [len(dataset[column].cat.categories) for column in categorical_columns] categorical_embedding_sizes = [(col_size, min(50, (col_size+1)//2)) for col_size in categorical_column_sizes] print(categorical_embedding_sizes) #[(4, 2), (4, 2), (4, 2), (3, 2), (3, 2), (3, 2)]
(모든 범주형 칼럼의 고유값 수, 차원의 크기) 형태로 배열을 출력한 결과이다.
데이터 셋 분리
데이터를 훈련(0.8)과 테스트(0.2) 용도로 분리한다.
total_records = 1728 test_records = int(total_records * .2) #전체 데이터 중 20%를 테스트 용도로 사용 categorical_train_data = categorical_data[:total_records - test_records] categorical_test_data = categorical_data[total_records - test_records:total_records] train_outputs = outputs[:total_records - test_records] test_outputs = outputs[total_records - test_records:total_records]
데이터셋을 잘 분할했는지 확인하기 위해 레코드 개수를 출력한다. 다음은 훈련 및 테스트 용도의 레코드 개수를 출력한 결과이다.
print(len(categorical_train_data)) print(len(train_outputs)) print(len(categorical_test_data)) print(len(test_outputs)) """ 1383 1383 345 345 """
모델 네트워크 생성
class Model(nn.Module): def __init__(self, embedding_size, output_size, layers, p=0.4): super().__init__() self.all_embeddings = nn.ModuleList([nn.Embedding(ni,nf) for ni, nf in embedding_size]) self.embedding_dropout = nn.Dropout(p) all_layers = [] num_categorical_cols = sum((nf for ni, nf in embedding_size)) input_size = num_categorical_cols #입력층의 크기를 찾기 위해 범주형 피쳐 개수를 input_size 변수에 저장 for i in layers: all_layers.append(nn.Linear(input_size, i)) all_layers.append(nn.ReLU(inplace=True)) all_layers.append(nn.BatchNorm1d(i)) all_layers.append(nn.Dropout(p)) input_size = i all_layers.append(nn.Linear(layers[-1], output_size)) self.layers = nn.Swquential(*all_layers) def forward(self, x_categorical): embeddings = [] for i,e in enumerate(self.all_embeddings): embeddings.append(e(x_categorical[:,i])) x = torch.cat(embeddings, 1) x = self.embedding_dropout(x) x = self.layers(x) return x
- class 형태로 구현되는 모델은 nn.Module 을 상속받는다.
- __init__은 모델에서 사용될 파라미터와 신경망을 초기화하기 위한 용도로 사용하며, 객체가 생성될 때 자동으로 호출된다. init()에서 전달되는 매개변수는 다음과 같다.
- self : 첫 번째 파라미터는 self를 지정해야 하며 자기 자신을 의미한다.
- embedding_size : 범주형 피처의 임베딩 크기
- output_size : 출력층의 크기
- layers : 모든 계층에 대한 목록
- p : 드롭아웃(기본값은 0.5)
- super().init() : 부모 클래스 (Model 클래스)에 접근할 때 사용하며, super는 self를 사용하지 않는 것에 주의해야 한다.
- 모델의 네트워크 계층을 구축하기 위해 for문을 이용하여 각 계층을 all_layers 목록에 추가한다. 추가된 계층은 다음과 같다
- Linear : 선형 계층은 입력 데이터에 선형 변환을 진행한 결과이다. 선형 변환을 위해서는 다음 수식을 사용한다. y = Wx + b (W:가중치 , y: 선형 계층의 출력, x:입력값, b:bias)
- ReLU : activation function 으로 사용
- BatchNorm1d : batch normoalization 위해 사용 (데이터의 평균과 분산을 0,1이 되도록 )
- Dropout : overfitting 방지 위해 사용
- forward() : 학습 데이터를 입력받아서 연산을 진행한다. 모델 객체를 데이터와 함께 호출하면 자동으로 실행된다.
Model 클래스 객체 생성
모델 훈련을 위해 앞에서 정의한 Model클래스 객체를 생성한다. → 객체를 생성하면서 (범주형 피처의 임베딩 크기, 출력 크기, 은닉층의 뉴런, 드롭아웃)을 전달한다. 여기에서는 은닉층을 [200,100,50]으로 정의했지만 다른 크기로 지정하여 테스트해보는 것도 학습하는데에 도움이 된다.
model = Model(categorical_embedding_sizes, 4, [200,100,50], p=0.4) print(model) """ 코드를 실행하면 모델에 대한 네트워크를 보여준다. Model( (all_embeddings): ModuleList( (0-2): 3 x Embedding(4, 2) (3-5): 3 x Embedding(3, 2) ) (embedding_dropout): Dropout(p=0.4, inplace=False) (layers): Sequential( (0): Linear(in_features=12, out_features=200, bias=True) (1): ReLU(inplace=True) (2): BatchNorm1d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (3): Dropout(p=0.4, inplace=False) (4): Linear(in_features=200, out_features=100, bias=True) (5): ReLU(inplace=True) (6): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (7): Dropout(p=0.4, inplace=False) (8): Linear(in_features=100, out_features=50, bias=True) (9): ReLU(inplace=True) (10): BatchNorm1d(50, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (11): Dropout(p=0.4, inplace=False) (12): Linear(in_features=50, out_features=4, bias=True) ) ) """
모델의 파라미터 정의
모델을 훈련시키기 전에 loss function과 optimizer를 정의해야 한다.
이번 예제는 classification을 하기 위해 loss는 cross entropy를 사용하고 ,optimizer로는 Adam을 사용한다.
loss_function = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
CPU/GPU 사용지정 코드
파이토치는 GPU에 최적화된 딥러닝 프레임워크이다. 하지만 GPU가 없다면 CPU를 사용하도록 지정해주어야 한다. GPU가 있다면 GPU를 사용하고 없다면 CPU를 사용하도록 하는 코드이다,
if torch.cuda.is_availabel(): device = torch.device('cuda') #GPU가 있다면 GPU사용 else: device = torch.device('cpu') #GPU가 없다면 CPU사용
훈련 데이터셋으로 모델학습
**epochs = 500 aggregated_losses = [] train_outputs = train_outputs.to(device=device, dtype=torch.int64) for i in range(epochs): #500회 반복, 각 반복마다 loss function이 오차를 계산 i +=1 y_pred = model(categorical_train_data).to(device) single_loss = loss_function(y_pred, train_outputs) aggregated_losses.append(single_loss) #반복할 때마다 오차를 aggregated_losses에 추가 if i%25 ==1: print(f'epoch: {i:3} loss: {single_loss.item():10.8f}') optimizer.zero_grad() single_loss.backward() #가중치를 업데이트하기 위해 손실 함수의 backward()호출 optimizer.step() #옵티마이저 함수의 step()을 이용하여 기울기 업데이트 print(f'epoch: {i:3} loss: {single_loss.item():10.10f}') #오차가 25 에포크 마다 출력 """ epoch: 1 loss: 0.56075656 epoch: 26 loss: 0.56370813 epoch: 51 loss: 0.56282055 epoch: 76 loss: 0.56012213 epoch: 101 loss: 0.55958372 epoch: 126 loss: 0.55970919 epoch: 151 loss: 0.56023580 epoch: 176 loss: 0.55706167 epoch: 201 loss: 0.55358350 epoch: 226 loss: 0.56061119 epoch: 251 loss: 0.55711693 epoch: 276 loss: 0.55713391 epoch: 301 loss: 0.55609691 epoch: 326 loss: 0.55222392 epoch: 351 loss: 0.55912113 epoch: 376 loss: 0.55615872 epoch: 401 loss: 0.55248874 epoch: 426 loss: 0.55285758 epoch: 451 loss: 0.55347329 epoch: 476 loss: 0.54691380 epoch: 500 loss: 0.5514589548"""**
출력 결과는 25 에포크마다 나타나는 오차 정보이다. 학습이 끝났으므로, 테스트 데이터셋으로 예측을 진행한다. categorical_test_data 데이터셋을 모델에 적용한다.
테스트 데이터셋으로 모델 예측
test_outputs = test_outputs.to(device=device, dtype=torch.int64) with torch.no_grad(): y_val = model(categorical_test_data).to(device) loss = loss_function(y_val, test_outputs) print(f'Loss: {loss:.8f}') #Loss: 0.54374832
출력결과는 테스트 용도 데이터셋에 대한 LOSS값(약 0.54)이다. 이 값은 훈련 데이터셋에서 도출된 LOSS값(약 0.55 )과 비슷하므로 overfitting은 발생하지 않았다고 판단할 수 있다.
모델의 예측 확인
테스트 데이터 셋을 이용했을 때 모델이 얼마나 잘 예측했는지 살펴보자. 앞에서 모델 네트워크의 output_size를 4로 했으므로 , 각 예측에는 네 개의 뉴런에 대한 예측값이 포함된다.
살펴보면 각 예측마다 인덱스 0번째 예측 값이 가장 높은 값을 갖는 것을 볼 수 있다.
print(y_val[:5]) """ tensor([[ 2.8653, 1.7405, -3.9987, -3.4999], [ 1.6895, 0.7262, -3.2320, -3.0281], [ 2.1368, 1.2442, -2.5835, -2.6153], [ 3.5625, 2.0839, -5.9175, -5.8720], [ 1.8620, 0.8155, -4.2863, -4.1634]])"""
argmax: 가장 큰 값을 갖는 인덱스 확인
y_val = np.argmax(y_val.cpu().numpy(), axis=1) print(y_val[:5]) """ tensor([0,0,0,0,0]) """
모델의 예측결과를 평가에 적합한 방식으로 변환하기 위해 수행한다.
cpu() : .numpy()와 같은 작업은 GPU텐서에 직접 이용할 수 없기 때문에 .cpu()로 변환하는 작업이 필요하다.
y_val.cpu().numpy : 파이토치 텐서를 Numpy배열로 변경한다.
np.argmax : 최대값의(가장 높은 신뢰 점수) 인덱스를 찾는 함수, 가장 높은 수가 i번째 인덱스에 있다면 i를 출력하는 함수이다. 그래서 출력 결과가 0번째 인덱스를 나타내는 0,0,0,0,0으로 나타난 것이다.
요약하자면, 이 코드는 모델의 예측을 간단한 레이블 인코딩 형식으로 변환한다.(argmax이용) , 가장 예측 점수가 높은 클래스 레이블로 변환하여 평가 지표로 사용한다.
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score # Confusion Matrix print("Confusion Matrix:") print(confusion_matrix(test_outputs, y_val)) # Classification Report print("\\nClassification Report:") print(classification_report(test_outputs, y_val)) # Accuracy Score print("Accuracy Score:", accuracy_score(test_outputs, y_val)) """ Confusion Matrix: [[259 0 0] [ 85 0 1] [ 0 0 0]] Classification Report: precision recall f1-score support 0 0.75 1.00 0.86 259 1 0.00 0.00 0.00 86 3 0.00 0.00 0.00 0 accuracy 0.75 345 macro avg 0.25 0.33 0.29 345 weighted avg 0.57 0.75 0.64 345 """
모든 파라미터를 무작위로 선택했다는 것을 감안할 때 75%의 accuracy는 나쁘지 않다. 파라미터(훈련/테스트셋 분할 비율, 은닉층 개수 및 크기 등)을 변경하면서 더 나은 성능을 찾아보는 것도 학습에 도움이 될 것이다.
성능 평가 지표
- True Positive : 모델이 1이라고 예측했는데 실제값도 1인경우
- True Negative : 모델이 0이라고 예측했는데 실제값도 0인경우
- False Positive : 모델이 1이라고 예측했는데 실제값은 0인경우 (Type 1 오류)
- False Negative : 모델이 0이라고 예측했는데 실제값은 1인경우(Type 2 오류)
Accuracy
⇒ 전체 예측 건수에서 정답을 맞힌 건수의 비율 , 이때 정답이 positive이든 negative이든 상관없다.
(TP + TN)/TP + TN + FP + FN
Recall
⇒ 실제로 정답이 1이라고 할 때 모델도 1로 예측한 비율, 처음부터 데이터가 1일 확률이 적을 때 사용하면 좋다.
TP / (TP +FN)
Precision
⇒ 모델이 1이라고 예측한 것 중에서 실제로 정답이 1인 비율
TP / (TP + FP)
F1-score
⇒ precision과 recall은 trade-off 관계이다. 이 문제를 해결하기 위해 precision과 recall의 조화 평균을 이용한 것이 f1 score이다.
2* [(Precision * Recall) / (Precision + Recall)]
"딥러닝 파이토치 교과서"를 보고 공부하였습니다. 아래 실습 코드가 있습니다.
https://github.com/gilbutITbook/080289
GitHub - gilbutITbook/080289
Contribute to gilbutITbook/080289 development by creating an account on GitHub.
github.com
'ML,DL' 카테고리의 다른 글
LG AIMERS 4기 PHASE 2 해커톤 및 수료 후기 (0) 2024.03.11 Logistic regression을 사용한 예측 모델 만들기 (1) 2024.01.23