Post
Ch2 파이토치 코드 맛보기 | Gihun Son

Ch2 파이토치 코드 맛보기

  1. price(자동차 가격)

  2. maint(자동차 유지 비용)

  3. doors(자동차 문 개수)

  4. persons(수용 인원)

  5. lug_capacity(수하물 용량)

  6. safety(안전성)

  7. output(차 상태): 이 데이터는 unacc(허용 불가능한 수준) 및 acc(허용 가능한 수준), 양호(good) 및 매우 좋은(very good, vgood) 중 하나의 값을 갖는다.

이때 1~6의 column 정보를 이용하여 일곱 번째 칼럼(차 상태)을 예측하는 코드를 구현

1
2
3
4
5
6
7
8
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns #데이터 프레임으로 다양한 통계 지표를 표현할 수 있는 시각화 차트 제공
#notebook을 실행한 브라우저에서 바로 그림을 볼 수 있게 해주는 것
%matplotlib inline
1
2
dataset=pd.read_csv('./dataset/car_evaluation.csv')
dataset.head() #Dataframe 내의 처음 n줄을 출력하여 내용 확인 가능(n의 기본값은 5)

5개의 행이 숫자와 단어로 구성되어 있다. 컴퓨터는 단어를 인식할 수 없기 때문에 단어를 Vector로 바꿔주는 Embedding처리가 필요하다.

주어진 데이터셋을 이해하기 쉽도록 분포 형태로 시각화하여 표현하면 다음과 같다.

1
2
3
4
5
6
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%의 자동차는 허용 불가능한 상태이다. 즉 양호한 상태의 자동차 비율이 낮다.

이 정보를 바탕으로 data preprocessing을 한다.

딥러닝은 통계 알고리즘을 기반으로 하기 때문에 단어를 숫자(Tensor)로 변환해주어야 한다. 주어진 데이터의 형태를 파악한 후 숫자로 변환해준다.

위 데이터를 보면 모두 범주형(category) 데이터(ex. 여자, 남자)이다.

1
categorical_columns = ['price', 'maint', 'doors', 'persons', 'lug_capacity', 'safety'] #dataset의 column 목록
1
2
for category in categorical_columns:
    dataset[category] = dataset[category].astype('category') #astype() method를 사용하여 데이터를 categorical(범주형)로 변환

categorical data를 tensor로 변환하기 위해서는 다음과 같은 절차가 필요하다.

categorical data -> dataset[category] -> Numpy 배열 -> Tensor

따라서 categorical data(단어)를 Numpy 배열(숫자)로 변환하기 위해 cat.codes를 사용한다.

(단 cat.codes는 어떤 class가 어떤 숫자로 mapping되어있는지 확인하기 어렵다는 단점이 있으므로 주의)

1
2
3
4
5
6
7
8
9
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

categorical_data = np.stack([price, maint, doors, persons, lug_capacity, safety], 1)  #np.stak은 2개 이상의 numpy 객체를 합칠 때 사용
categorical_data[:10] #합친 numpy 배열 중 10개의 row를 출력하여 보여준다.

Note) np.stack과 np.concatenate

넘파이 객체를 합칠 때 사용하는 메서드로는 np.stack과 np.concatenate가 있습니다. 이 두 메서드는 차원의 유지 여부에 대한 차이가 있다.

image.png

image.png

np.concatenate는 다음 그림과 같이 선택한 축(axis)을 기준으로 두 개의 배열을 연결

image.png

image.png

하지만 np.stack은 배열들을 새로운 축으로 합쳐 준다. 예를 들어 1차원 배열들을 합쳐서 2차원 배열을 만들거나 2차원 배열 여러 개를 합쳐 3차원 배열을 만든다. 따라서 반드시 두 배열의 차원이 동일해야 한다.

예시 코드)

1
2
3
4
5
6
a = np.array([[1, 2], [3, 4]]) #------ a.shape=(2, 2)
b = np.array([[5, 6], [7, 8]]) #------ b.shape=(2, 2)
c = np.array([[5, 6], [7, 8], [9, 10]]) #------ c.shape=(3, 2)
print(np.concatenate((a, b), axis=0)) #------ shape=(4, 2)
print('-------------------------------')
print(np.stack((a, b), axis=0)) #------ shape=(2, 2, 2)

1
categorical_data[:10] #합친 numpy 배열 중 10개의 row를 출력하여 보여준다.

위 결과는 categorical data에서 numpy 배열로 변환된 데이터 중 10개의 row를 출력한 결과이다. 이제 이 배열을 torch모듈을 사용하여 tensor로 변환해보자.

1
2
categorical_data=torch.tensor(categorical_data,dtype=torch.int64)
categorical_data[:10]

마지막으로 Label(outputs)로 사용할 column에 대해서도 Tensor로 변환

get_dummies를 이용해보겠다.

get_dummies는 dummy variable(가변수)로 만들어주는 함수이다. 가변수로 만들어준다는 의미는 문자를 숫자(0,1)로 바꾸어준다는 의미이다.


Ex)

get_dummies()함수를 적용한 아래 결과를 보면 원래 숫자값을 가졌던 몸무게는 변화가 없고, 성별과 국적만 0과 1로 변경된 것을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
import pandas as pd
import numpy as np

data = {
    'gender' : ['male','female','male'],
    'weight' : [72,55,68],
    'nation' : ['Japan','Korea','Australia']
}

df = pd.DataFrame(data)
df
1
pd.get_dummies(df)

Note)

ravel(), reshape(), flatten()

ravel(), reshape(), flatten()은 텐서의 차원을 바꿀 때 사용

Ex code) 아래와 같이 사용하면 2차원 텐서가 1차원 텐서로 변경된다.

1
2
3
4
a = np.array([[1, 2], [3, 4]])
print(a.ravel())
print(a.reshape(-1))
print(a.flatten())

1
2
3
4
5
6
7
8
outputs = pd.get_dummies(dataset.output) #label column을 텐서로 만들어주기 위한 과정
outputs = outputs.values
outputs = torch.tensor(outputs).flatten() #1차원 tensor로 변환

print(categorical_data.shape)
print(categorical_data)
print(outputs.shape)
print(outputs)

word embedding은 유사한 단어끼리 유사하게 인코딩되도록 표현하는 방법이다. 또한 높은 차원의 embedding일수록 단어간의 세부적인 관계를 잘 파악할 수 있다.

->따라서 단일 숫자로 embedding된 numpy배열을 N차원으로 변경하여 사용한다.

배열을 N차원으로 변환하기 위해 모든 categorical column에 대한 임베딩 크기(벡터 차원)를 정의한다.

임베딩 크기에 대한 정확한 규칙은 없지만, column의 고유값 수를 2로 나누는 것을 많이 사용한다.

Ex) ‘price’ column은 4개의 고유값(vhigh, high, med, low)을 갖기 때문에 임베딩 크기는 4/2=2이다.

다음 코드를 통해 (모든 categorical column의 고유값 수, 차원의 크기)형태로 배열을 만든다.

1
2
3
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)

데이터셋을 train과 test용도로 분리한다.

1
2
3
4
5
6
7
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]
1
2
3
4
print(len(categorical_train_data))
print(len(train_outputs))
print(len(categorical_test_data))
print(len(test_outputs))

다음은 모델의 네트워크를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.Sequential(*all_layers) # ------ 신경망의 모든 계층이 순차적으로 실행되도록 모든 계층에 대한 목록(all_layers)을 nn.Sequential 클래스로 전달

    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) #------ 넘파이의 concatenate와 같지만 대상이 텐서가 된다.
        x = self.embedding_dropout(x)
        x = self.layers(x)
        return x

① 클래스(class) 형태로 구현되는 모델은 nn.Module을 상속받는다.

init()은 모델에서 사용될 파라미터와 신경망을 초기화하기 위한 용도로 사용하며, 객체가 생성될 때 자동으로 호출된다. init()에서 전달되는 매개변수는 다음과 같다. image.png

  • ⓐ self: 첫 번째 파라미터는 self를 지정해야 하며 자기 자신을 의미한다. 예를 들어 ex라는 함수가 있을 때 self 의미는 다음 그림과 같다.

image.png

  • ⓑ embedding_size: 범주형 칼럼의 임베딩 크기

  • ⓒ output_size: 출력층의 크기

  • ⓓ layers: 모든 계층에 대한 목록

  • ⓔ p: 드롭아웃(기본값은 0.5)

③ super().init()은 부모 클래스(Model 클래스)에 접근할 때 사용하며 super는 self를 사용하지 않는 것에 주의해야 함.

④ 모델의 네트워크 계층을 구축하기 위해 for 문을 이용하여 각 계층을 all_layers 목록에 추가한다. 추가된 계층은 다음과 같다.

  • Linear: 선형 계층(linear layer)은 입력 데이터에 선형 변환을 진행한 결과. 선형 변환을 위해서는 다음 수식을 사용한다.

y = Wx + b

(y: 선형 계층의 출력 값, W: 가중치, x: 입력 값, b: 바이어스)

따라서 선형 계층은 입력과 가중치를 곱한 후 바이어스를 더한 결과이다.

  • ReLU: 활성화 함수로 사용

  • BatchNorm1d: 배치 정규화(batch normalization)5 용도로 사용

  • Dropout: 과적합 방지에 사용

⑤ forward() 함수는 학습 데이터를 입력받아서 연산을 진행한다. forward() 함수는 모델 객체를 데이터와 함께 호출하면 자동으로 실행된다.

모델 훈련을 위해 앞에서 정의했던 Model 클래스의 객체를 생성한다. 객체를 생성하면서 (범주형 칼럼의 임베딩 크기, 출력 크기, 은닉층의 뉴런, 드롭아웃)을 전달한다. 여기에서는 은닉층을 [200,100,50]으로 정의

1
2
model = Model(categorical_embedding_sizes, 4, [200,100,50], p=0.4) #모델의 객체 생성
print(model)

위는 모델의 Architecture를 보여준다.

이제 모델을 학습시키기 전, Loss function과 Optimizer를 정의해야 한다.

예제는 데이터를 Classification하는 것이므로 Cross entropy loss function을 사용한다. Optimizer는 Adam을 사용한다.

1
2
loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

파이토치는 GPU에 최적화된 deeplearning framework이므로 GPU가 있다면 사용해준다.

1
2
3
4
if torch.cuda.is_available():
    device = torch.device('cuda')# ------ GPU가 있다면 GPU를 사용
else:
    device = torch.device('cpu')# ------ GPU가 없다면 CPU를 사용

이제 모델 학습에 필요한 준비가 다 되었다. 준비된 데이터를 이용하여 모델을 학습시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
epochs = 500
aggregated_losses = []
train_outputs = train_outputs.to(device=device, dtype=torch.int64)
for i in range(epochs):# ------ for 문은 500회 반복되며, 각 반복마다 손실 함수가 오차를 계산
    i += 1
    y_pred = model(categorical_train_data)
    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 에포크마다 출력

500epoch의 학습이 끝났으므로 test dataset으로 결과를 확인해보자

1
2
3
4
5
test_outputs = test_outputs.to(device=device, dtype=torch.int64)
with torch.no_grad():
    y_val = model(categorical_test_data)
    loss = loss_function(y_val, test_outputs)
print(f'Loss: {loss:.8f}')

위 결과를 통해 Training의 loss와 유사하게 나왔으므로 overfitting은 발생하지 않았음을 알 수 있다.

이전에 모델 Architecture에서 output_size=4로 지정했기 때문에 ouput layer에 4개의 Neural이 포함되도록 지정됨. -> 각 예측값에는 4개의 값이 포함될 것이다.

1
print(y_val[:5])
1
2
y_val = np.argmax(y_val.cpu().numpy(), axis=1)
print(y_val[:5])
1
2
3
4
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
print(confusion_matrix(test_outputs,y_val))
print(classification_report(test_outputs,y_val))
print(accuracy_score(test_outputs, y_val))
This post is licensed under CC BY 4.0 by the author.