본문 바로가기

카테고리 없음

[ML 머신러닝] Multiple Logistic Regression 다항 로지스틱 회귀 분석

데이터에 3개 이상의 다양한 입력 변수가 있을 때, 사용하는 Logistic Regression을 Multiple Logistic Regression이라고 합니다. 이번 편에서는 물품의 상태를 판별하는 이미지 데이터를 가지고 진행해봅니다.

 

Step 1. Images of metal-casting parts

제조업 분야에서 물품의 상태를 판별하는데 컴퓨터 비전(CV)을 많이 사용합니다. (컴퓨터 비전은 이미지 데이터를 이용한 인공지능의 한 분야입니다.) 물품의 사진이 주어지면 그것이 결함이 있는지 없는지 판단하는 모델을 multiple logistic regression으로 진행해보겠습니다.

먼저 간단한 실험을 하기 위해, 이미지를 흑백으로 변환하고, 적은 개수의 데이터로 진행합니다.

시각화를 돕는 코드를 먼저 실행합니다. (코랩에서 진행하는 경우 실행합니다.)

if 'google.colab' in str(get_ipython()):
    print('Downloading plot_helpers.py to util/ (only ne ded for colab')
    !mkdir util; wget https://raw.githubusercontent.com/minireference/noBSLAnotebooks/master/util/plot_helpers.py -P util

필요한 패키지를 import하고, 첨부되어 있는 파일을 통해 데이터를 불러옵니다.

from autograd import numpy
from autograd import grad
from matplotlib import pyplot
from urllib.request import urlretrieve
URL = 'https://github.com/engineersCode/EngComp6_deeplearning/raw/master/data/casting_images.npz'
urlretrieve(URL, 'casting_images.npz')

# read in images and labels
with numpy.load("/content/casting_images.npz", allow_pickle=True) as data:
    ok_images = data["ok_images"]
    def_images = data["def_images"]

데이터가 잘 불러와졌는지 확인합니다.

ok_images.shape

519는 우리의 전체 데이터의 개수입니다.

원래 데이터는 128*128 사이즈의 이미지 데이터입니다. 그것을 하나로 쭉 펴서 다룰 것이기 때문에 16384(128*128)가 나왔습니다.

이제 데이터셋이 어떻게 구성되는지 알아봅니다.

n_ok_total = ok_images.shape[0]
res = int(numpy.sqrt(def_images.shape[1]))

print("Number of images without defects:", n_ok_total)
print("Image resolution: {} by {}".format(res, res))

정상적인 제품의 이미지입니다.

fig, axes = pyplot.subplots(2, 3, figsize=(8, 6), tight_layout=True)
axes[0, 0].imshow(ok_images[0].reshape((res, res)), cmap="gray")
axes[0, 1].imshow(ok_images[50].reshape((res, res)), cmap="gray")
axes[0, 2].imshow(ok_images[100].reshape((res, res)), cmap="gray")
axes[1, 0].imshow(ok_images[150].reshape((res, res)), cmap="gray")
axes[1, 1].imshow(ok_images[200].reshape((res, res)), cmap="gray")
axes[1, 2].imshow(ok_images[250].reshape((res, res)), cmap="gray")
fig.suptitle("Casting parts without defects", fontsize=20);

 

결함 있는 제품의 이미지입니다.

fig, axes = pyplot.subplots(2, 3, figsize=(8, 6), tight_layout=True)
axes[0, 0].imshow(def_images[0].reshape((res, res)), cmap="gray")
axes[0, 1].imshow(def_images[50].reshape((res, res)), cmap="gray")
axes[0, 2].imshow(def_images[100].reshape((res, res)), cmap="gray")
axes[1, 0].imshow(def_images[150].reshape((res, res)), cmap="gray")
axes[1, 1].imshow(def_images[200].reshape((res, res)), cmap="gray")
axes[1, 2].imshow(def_images[250].reshape((res, res)), cmap="gray")
fig.suptitle("Casting parts with defects", fontsize=20);

 

Step 2. Multiple logistic regression

 

지난 시간에 logistic regression을 배우면서 logistic function을 같이 배웠습니다. Logistic function은 출력 값이 0과 1 사이의 확률 값이 되도록 변환해주는 함수입니다. 그래서 지금 같이 분류해야할 class가 2개 일 때 많이 사용합니다. 

지난 시간과 이번의 차이점은 지난번에는 입력 변수가 1개였다면 이번에는 여러개의 입력 변수가 있습니다. 수식을 통해서 알아보도록 하겠습니다. 

$$\hat{y}^{(1)} = \text{logistic}(b + w_1x_1^{(1)}+ w_2x_2^{(1)} + ... + w_nx_n^{(1)})$$
$$\hat{y}^{(2)} = \text{logistic}(b + w_1x_1^{(2)}+ w_2x_2^{(2)} + ... + w_nx_n^{(2)})$$
$$\vdots$$
$$\hat{y}^{(N)} = \text{logistic}(b + w_1x_1^{(N)}+ w_2x_2^{(N)} + ... + w_nx_n^{(N)})$$

위 식에서 $(1), (2), ... (N)$은 $N$개의 이미지가 있다는 것입니다. $\hat{y}$는 예측한 확률 값입니다. 

위의 수식들을 행렬의 형태로 바꾸면 다음과 같이 바꿀 수 있습니다. 

$$
\begin{bmatrix}
\hat{y}^{(1)} \\
\vdots        \\
\hat{y}^{(N)}
\end{bmatrix} 

\text{logistic} \left(
\begin{bmatrix}
b             \\
\vdots        \\
b
\end{bmatrix}
+
\begin{bmatrix}
x_1^{(1)} & \cdots & x_n^{(1)} \\
\vdots    & \ddots & \vdots    \\
x_1^{(N)} & \cdots & x_n^{(N)} 
\end{bmatrix}
\begin{bmatrix}
w_1             \\
\vdots        \\
w_n
\end{bmatrix}
\right)
$$

$$\hat{\mathbf{y}} = \text{logistic}(\mathbf{b} + \mathbf{X} \mathbf{w})$$

이제 코드를 통해서 함수로 만들어줍니다.

def logistic(x):
    """Logistic/sigmoid function.
    
    Arguments
    ---------
    x : numpy.ndarray
        The input to the logistic function.
    
    Returns
    -------
    numpy.ndarray
        The output.
        
    Notes
    -----
    The function does not restrict the shape of the input array. The output
    has the same shape as the input.
    """
    out = 1. / (1. + numpy.exp(-x))

    return out
def logistic_model(x, params):
    """A logistic regression model.
    
    A a logistic regression is y = sigmoid(x * w + b), where the operator *
    denotes a mat-vec multiplication.
    
    Arguments
    ---------
    x : numpy.ndarray
        The input of the model. The shape should be (n_images, n_total_pixels).
    params : a tuple/list of two elemets
        The first element is a 1D array with shape (n_total_pixels). The
        second element is a scalar (the intercept)

    Returns
    -------
    probabilities : numpy.ndarray
        The output is a 1D array with length n_samples.
    """

    out = logistic(numpy.dot(x, params[0]) + params[1])

    return out

이제 cost function을 만들어봅니다. cost function은 Logistic regression $ L = -y\log(a)+(y-1)\log(1-a) $를 사용하고, 16384개의 많은 feature들을 가지고 있기 때문에 regularization term을 추가합니다. 

$$\text{cost function} = -\sum_{i=1}^N y_{\text{true}}^{(i)} \log\left(\hat{y}^{(i)}\right) + \left( 1- y_{\text{true}}^{(i)}\right) \log\left(1-\hat{y}^{(i)}\right) + \lambda \sum_{i=1}^n w_i^2 $$

이것을 벡터꼴로 나타내면 다음과 같습니다. 

$$\text{cost function} = - [\mathbf{y}_{\text{true}}\log\left(\mathbf{\hat{y}}\right) + \left( \mathbf{1}- \mathbf{y}_{\text{true}}\right) \log\left(\mathbf{1}-\mathbf{\hat{y}}\right)] + \lambda \sum_{i=1}^n w_i^2 $$

 

여기서 $\mathbf{1}$는 1로 이루어진 벡터입니다. 

cost function도 함수로 만들어줍니다.

def model_loss(x, true_labels, params, _lambda=1.0):
    """Calculate the predictions and the loss w.r.t. the true values.
    
    Arguments
    ---------
    x : numpy.ndarray
        The input of the model. The shape should be (n_images, n_total_pixels).
    true_labels : numpy.ndarray
        The true labels of the input images. Should be 1D and have length of
        n_images.
    params : a tuple/list of two elements
        The first element is a 1D array with shape (n_total_pixels). The
        second elenment is a scalar.
    _lambda : float
        The weight of the regularization term. Default: 1.0
    
    Returns
    -------
    loss : a scalar
        The summed loss.
    """
    pred = logistic_model(x, params)
    
    loss = - (
        numpy.dot(true_labels, numpy.log(pred+1e-15)) +
        numpy.dot(1.-true_labels, numpy.log(1.-pred+1e-15))
    ) + _lambda * numpy.sum(params[0]**2)
    
    return loss

Step 3. Training, Validation, and Test Datasets

모델을 학습하는 목적은 단순히 "주어진 데이터 셋을 잘 설명하기 위해서"만은 아닙니다. 주어진 데이터셋을 잘 설명하고 또 그로 인해 학습 때 본 데이터가 아닌 새롭게 주어진 데이터에서도 잘 예측 할 수 있는 그런 모델을 만들어야 합니다. 그래서 모델의 성능을 평가할 때 학습 때는 보지 못한 데이터로 성능을 측정해야 합니다. 그것이 모델의 최종 목적이기 때문입니다.

 

이것은 수험생들이 수능 공부를 하는 것과 유사합니다. 수험생들은 학교 수업을 듣고, 문제집을 풀면서 공부를 합니다. 그 이유는 수능 때 좋은 성적을 얻기 위함입니다. 이처럼 수험생들은 수많은 문제집이라는 Training data로 학습을 하고 Training data에서는 보지 못한 수능이라는 새로운 데이터에서 좋은 성능을 나타내는 것을 목표로 합니다.

 

이처럼 모델을 학습 할 때는 데이터셋을 Training, Validation, Test 이렇게 3가지로 나누게 됩니다. 학습을 진행 할 때는 Training과 Validation을 사용하고 최종 성능을 Test로 측정합니다.

왜 2가지(Training, Test)가 아니라 Validation까지 필요할까요?

 

모델은 한 번도 보지 못한 Test data에 대해서 잘 예측해야합니다. 그러면 Training으로 학습을 하면서 이렇게 학습을 하면 새로운 데이터에 잘 예측을 할 수 있는지 확인 해보고, 아니면 수정을 해야합니다. 그래서 존재하는 것이 Validation입니다. Training을 가지고 학습하는 과정에서 새로운 Validation에서도 잘 작동을 하도록 학습을 해줍니다. 이것은 앞선 예제어서 수험생들이 모의고사를 치르는 것과 유사합니다. 수험생들은 평소에 문제집을 풀 때 모의고사를 통해서 부족한 점을 파악하고 앞으로 어떻게 학습을 진행해 나갈지 정하게 됩니다.

 

그럼 전체 데이터 셋을 3종류로 나누어봅니다. 60%는 training, 20%는 validation, 마지막 20%는 test로합니다.

코드를 통해서 분배해봅니다.

# numbers of images for validation (~ 20%)
n_ok_val = int(n_ok_total * 0.2)
n_def_val = int(n_def_total * 0.2)
print("Number of images without defects in validation dataset:", n_ok_val)
print("Number of images with defects in validation dataset:", n_def_val)

# numbers of images for test (~ 20%)
n_ok_test = int(n_ok_total * 0.2)
n_def_test = int(n_def_total * 0.2)
print("Number of images without defects in test dataset:", n_ok_test)
print("Number of images with defects in test dataset:", n_def_test)

# remaining images for training (~ 60%)
n_ok_train = n_ok_total - n_ok_val - n_ok_test
n_def_train = n_def_total - n_def_val - n_def_test
print("Number of images without defects in training dataset:", n_ok_train)
print("Number of images with defects in training dataset:", n_def_train)

이제 numpy 패키지 안에 split 함수로 나누어줍니다.

ok_images = numpy.split(ok_images, [n_ok_val, n_ok_val+n_ok_test], 0)
def_images = numpy.split(def_images, [n_def_val, n_def_val+n_def_test], 0)

그리고 numpy 패키지 안에 concatenate 함수를 이용해서 train, val, test끼리 결함이 있는 이미지와 없는 이미지들을 합쳐줍니다.

images_val = numpy.concatenate([ok_images[0], def_images[0]], 0)
images_test = numpy.concatenate([ok_images[1], def_images[1]], 0)
images_train = numpy.concatenate([ok_images[2], def_images[2]], 0)

Step 4. Data normalization: z-score normalization

다양한 feature가 있으면 normalization을 해주어야 합니다. 이번에는 z-score normalization을 사용하겠습니다.

$$z = \frac{x - \mu_\text{train}}{\sigma_\text{train}}$$

Train, Validation, Test 모두 진행합니다.

# calculate mu and sigma
mu = numpy.mean(images_train, axis=0)
sigma = numpy.std(images_train, axis=0)

# normalize the training, validation, and test datasets
images_train = (images_train - mu) / sigma
images_val = (images_val - mu) / sigma
images_test = (images_test - mu) / sigma

Step 5. Creating labels/classes

이제 데이터셋에 class label을 정해주어야 합니다. 이 이미지가 결함이 있는지 없는지 명시적으로 나타내주는 것입니다.

결함이 있는 것을 1, 결함이 없는 것을 0으로 나타내어 줍니다.

# labels for training data
labels_train = numpy.zeros(n_ok_train+n_def_train)
labels_train[n_ok_train:] = 1.

# labels for validation data
labels_val = numpy.zeros(n_ok_val+n_def_val)
labels_val[n_ok_val:] = 1.

# labels for test data
labels_test = numpy.zeros(n_ok_test+n_def_test)
labels_test[n_ok_test:] = 1.

이제 입력으로 들어온 이미지에 결함이 있는지 없는지 알아내기 위해 Logistic model을 사용합니다.

출력 확률 값이 0.5보다 크면 결함이 있고 0.5보다 작으면 결함이 없다고 분류하는 함수를 작성합니다.

def classify(x, params):
    """Use a logistic model to label data with 0 or/and 1.
    
    Arguments
    ---------
    x : numpy.ndarray
        The input of the model. The shape should be (n_images, n_total_pixels).
    params : a tuple/list of two elements
        The first element is a 1D array with shape (n_total_pixels). The
        second element is a scalar.
    
    Returns
    -------
    labels : numpy.ndarray
        The shape of the label is the same with `probability`.
    
    Notes
    -----
    This function only works with multiple images, i.e., x has a shape of
    (n_images, n_total_pixels).
    """
    probabilities = logistic_model(x, params)
    labels = (probabilities >= 0.5).astype(float)
    return labels

step 6. evaluating model performance: F-score, Accuracy

이제 학습한 모델이 얼마나 잘 예측을 하는지 알아봅니다.

모델이 예측한 결과는 다음 4가지 종류로 분류할 수 있습니다.

  1. True Positive(TP) : 결함이 있다고 예측한 것들 중 실제로 결함이 있는 것
  2. False Positive(FP) : 결함이 있다고 예측한 것들 중에서 실제로 결함이 없는 것
  3. True Negative(TN) : 결함이 없다고 예측한 것들 중에서 실제로 결함이 없는 것
  4. False Negative(FN) : 결함이 없다고 예측한 것들 중에서 실제로 결함이 있는 것
  결함이 있다고 예측 결함이 없다고 예측
실제로 결함이 있음 $$N_{TP}$$ $$N_{TN}$$
실제로 결함이 없음 $$N_{FP}$$ $$N_{FN}$$

위에서 $N$은 개수를 나타냅니다. 

이제 위에서 설명한 것들을 가지고 가장 보편적으로 사용하는 지표 3가지를 알아보도록 하겠습니다. 

$$\text{accuracy} = \frac{\text{정확하게 예측한 개수}}{\text{예측한 전체 개수}}= \frac{N_{TP} + N_{TN}}{N_{TP}+N_{FN}+N_{FP}+N_{TN}}$$
$$\text{precision} = \frac{\text{결함이 있다고 정확하게 예측한 개수}}{\text{결함이 있다고 예측한 총 개수}} = \frac{N_{TP}}{N_{TP}+N_{FP}}$$
$$\text{recall} = \frac{\text{결함이 있다고 정확하게 예측한 개수}}{\text{실제로 결함이 있는 개수}} =\frac{N_{TP}}{N_{TP}+N_{FN}}$$

여기서 정밀도(Precision)와 재현율(Recall)로 F-score를 계산할 수 있습니다. 

$$\text{F-score} = \frac{(1+\beta^2) \text{precision} \times \text{recall}}{\beta^2 \text{precision} + \text{recall}}$$

$\beta$는 정밀도(Precision)와 재현율(Recall)중 어떤 것을 중점적으로 생각할지 우리가 결정하는 상수입니다.

 

정밀도(Precision)와 재현율(Recall) 사이에 우선순위를 정하는 것은 매우 중요한 작업입니다.

예를들어 CT 촬영 데이터를 가지고 암진단을 한다고 가정해봅니다. 실제로 암에 걸렸는데 안걸렸다고 판정하는것과 암에 안걸렸는데 걸렸다고 판정하는 것 두 가지중 어떤 것이 더 위험할까요?

환자가 암에 걸렸는데 안걸렸다고 판단을 내리는 것이 훨씬 위험할 것입니다. 이처럼 모델이 어떻게 사용되느냐에 따라서 둘 사이의 우선순위를 결정하는 것은 매우 중요한 일이 됩니다.

이제 accuracy와 f1-score을 구하는 함수를 코드로 작성해봅니다.

def performance(predictions, answers, beta=1.0):
    """Calculate precision, recall, and F-score.
    
    Arguments
    ---------
    predictions : numpy.ndarray of integers
        The predicted labels.
    answers : numpy.ndarray of integers
        The true labels.
    beta : float
        A coefficient representing the weight of recall.
    
    Returns
    -------
    precision, recall, score, accuracy : float
        Precision, recall, and F-score, accuracy respectively.
    """
    true_idx = (answers == 1)  # the location where the answers are 1
    false_idx = (answers == 0)  # the location where the answers are 0
    
    # true positive: answers are 1 and predictions are also 1
    n_tp = numpy.count_nonzero(predictions[true_idx] == 1)
    
    # false positive: answers are 0 but predictions are 1
    n_fp = numpy.count_nonzero(predictions[false_idx] == 1)
    
    # true negative: answers are 0 and predictions are also 0
    n_tn = numpy.count_nonzero(predictions[false_idx] == 0)
    
    # false negative: answers are 1 but predictions are 0
    n_fn = numpy.count_nonzero(predictions[true_idx] == 0)
    
    # precision, recall, and f-score
    precision = n_tp / (n_tp + n_fp)
    recall = n_tp / (n_tp + n_fn)
    score = (
        (1.0 + beta**2) * precision * recall / 
        (beta**2 * precision + recall)
    )

    accuracy = (n_tp + n_tn) / (n_tp + n_fn + n_fp + n_tn)

    return precision, recall, score, accuracy

Step 7. Initialization

이제 학습할 Parameter들을 초기화합니다. 먼저 0으로 초기화해서 성능을 측정해봅니다.

# a function to get the gradients of a logistic model
gradients = grad(model_loss, argnum=2)

# initialize parameters
w = numpy.zeros(images_train.shape[1], dtype=float)
b = 0.

학습 전, 후의 성능 비교를 위해 학습 전 test dataset에서 성능을 측정해봅니다.

# initial accuracy
pred_labels_test = classify(images_test, (w, b))
perf = performance(pred_labels_test, labels_test)

print("Initial precision: {:.1f}%".format(perf[0]*100))
print("Initial recall: {:.1f}%".format(perf[1]*100))
print("Initial F-score: {:.1f}%".format(perf[2]*100))
print("Initial Accuracy: {:.1f}%".format(perf[3]*100))

0으로 초기화 했는데 성능이 나쁘지 않아보입니다. 왜 그럴까요?

전체를 다 0으로 초기화 하게되면 우리 모델은 단순히 모든 것들이 다 결함이 있다고 예측을 하게 됩니다. 즉 103개의 결함이 없는 부품, 156개의 결함이 있는 부품을 가지고 있기 때문에 성능이 괜찮아 보이는 것입니다. 마치 T/F 문제를 풀 때 모든 것을 F로 찍은 학생이 F가 정답인 문제의 개수가 많을 때 높은 점수를 받는 것과 비슷한 것입니다.

 

Test와 Validation Set은 우리의 현실 데이터와 비슷해야합니다. 실제 제조 공정에서 결함이 있는 부품들이 저렇게 많지 않을 것이기 때문에 이는 좋은 데이터 셋이라고 볼 수 없습니다. 데이터셋을 구성하는 데에는 그 해당 도메인에 대한 지식이 들어가야 합니다.

 

하지만 지금은 단순 실습이기 때문에 계속 진행해봅니다.

 

Step 8. Training / Optimization

이제 본격적으로 학습을 진행하도록 합니다. 학습을 진행하는 동안 Validation data로 얼마나 학습이 잘 진행되고 있는지 확인해봅니다. 그리고 Validation loss가 더이상 줄어들지 않는 부분에서 학습을 멈추도록 합니다.

# learning rate
lr = 1e-5

# a variable for the change in validation loss
change = numpy.inf

# a counter for optimization iterations
i = 0

# a variable to store the validation loss from the previous iteration
old_val_loss = 1e-15

# keep running if:
#   1. we still see significant changes in validation loss
#   2. iteration counter < 10000
while change >= 1e-5 and i < 10000:
    
    # calculate gradients and use gradient descents
    grads = gradients(images_train, labels_train, (w, b))
    w -= (grads[0] * lr)
    b -= (grads[1] * lr)
    
    # validation loss
    val_loss = model_loss(images_val, labels_val, (w, b))
    
    # calculate f-scores against the validation dataset
    pred_labels_val = classify(images_val, (w, b))
    score = performance(pred_labels_val, labels_val)

    # calculate the chage in validation loss
    change = numpy.abs((val_loss-old_val_loss)/old_val_loss)

    # update the counter and old_val_loss
    i += 1
    old_val_loss = val_loss
    
    # print the progress every 10 steps
    if i % 10 == 0:
        print("{}...".format(i), end="")

print("")
print("")
print("Upon optimization stopped:")
print("    Iterations:", i)
print("    Validation loss:", val_loss)
print("    Validation precision:", score[0])
print("    Validation recall:", score[1])
print("    Validation F-score:", score[2])
print("    Validation Accuracy:", score[3])
print("    Change in validation loss:", change)

최종 성능은 Test dataset으로 측정해야합니다! 이제 최종 성능을 구해봅니다.

# final accuracy
pred_labels_test = classify(images_test, (w, b))
perf = performance(pred_labels_test, labels_test)

print("Final precision: {:.1f}%".format(perf[0]*100))
print("Final recall: {:.1f}%".format(perf[1]*100))
print("Final F-score: {:.1f}%".format(perf[2]*100))
print("Final Accuracy: {:.1f}%".format(perf[3]*100))

F-score는 75.2%에서 86.2%, Accuracy는 60.2%에서 83.8%로 많이 증가했습니다.

이로써 모델의 학습이 잘 된 것을 확인할 수 있습니다.