이는 ROC 및 유사한 곡선을 처음부터 생성해야 하는 인기 있는 데이터 과학 인터뷰 질문 중 하나입니다. 즉, 데이터가 없습니다. 이 이야기의 목적을 위해 나는 독자들이 이러한 지표의 의미와 계산, 그리고 그것이 무엇을 나타내고 어떻게 해석되는지 알고 있다고 가정할 것입니다. 따라서 구현 측면에 중점을 두겠습니다. 필요한 라이브러리를 가져오는 것부터 시작합니다(해당 모듈이 계산에 사용되기 때문에 수학도 가져옵니다).
import pandas as pd import numpy as np import matplotlib.pyplot as plt import math
첫 번째 단계는 1(불량)과 0(양호)의 '실제' 데이터를 생성하는 것입니다. 이는 앞서 언급한 측정항목을 통해 모델 정확도를 계산하고 비교하는 데 사용되기 때문입니다. 이 기사에서는 균일 분포에서 "실제 벡터"를 생성합니다. 후속 기사 및 관련 기사에서는 이항 분포를 사용하겠습니다.
actual = np.random.randint(0, 2, 10000)
위의 코드는 실제 바이너리 클래스의 벡터인 [0,1]에 속하는 10,000개의 임의의 정수를 생성합니다. 이제 물론 이러한 실제 클래스에 대한 또 다른 확률 벡터가 필요합니다. 일반적으로 이러한 확률은 기계 학습 모델의 출력입니다. 그러나 여기서는 몇 가지 유용한 가정을 통해 무작위로 생성하겠습니다. 기본 모델이 '로지스틱 회귀 모델'이므로 연결 함수가 로지스틱 또는 로짓이라고 가정해 보겠습니다.
아래 그림은 표준 로지스틱 기능을 설명합니다. 로지스틱 회귀 모델의 경우 -k(x-x_0) 표현식은 '점수'로 대체됩니다. '점수'는 모델 기능과 모델 매개변수의 가중치 합계입니다.
따라서 'score' = 0인 경우 로지스틱 함수는 Y축에서 0.5를 통과해야 합니다. 이는 logit(p) = log-odds(p) = log(p/(1-p)) = 0 => p = 1-p => p =0.5이기 때문입니다. 또한 '점수'가 높은 양수 또는 높은 음수 값에 도달하면 함수는 점근적으로 1(나쁨) 또는 0(좋음)을 향해 이동합니다. 따라서 '점수'의 절대값이 높을수록 예측 확률도 높아집니다. 하지만 우리는 무엇을 득점하고 있습니까? 우리는 '실제 벡터'에 있는 각 데이터 입력의 점수를 매깁니다. 그런 다음 기본 로지스틱 회귀 모델이 숙련된, 즉 예측적이라고 가정하고 싶다면; 모델은 나쁜 점과 좋은 점에 상대적으로 더 높은 점수를 할당해야 합니다. 따라서 불량품은 더 많은 양수 점수를 가져야 하며(예측 확률이 1에 가까워지도록 보장하기 위해) 상품은 더 많은 음수 점수를 가져야 합니다(예측 확률이 0에 가까워지도록 보장하기 위해). 이를 모델별 순위 순서라고 합니다. 즉, 점수 사이에 차별이나 분리가 있어야 하며 그에 따라 불량과 양품의 예측 확률이 있어야 합니다. 이후 우리는 점수 0이 좋을 확률 = 나쁠 확률 = 0.5를 의미한다는 것을 확인했습니다. 이는 모델이 좋은 것과 나쁜 것을 구별할 수 없다는 것을 의미합니다. 그러나 데이터 포인트가 실제로 좋거나 나쁠 것이라는 것을 알고 있으므로 0.5점은 모델에서 가능한 최악의 점수입니다. 이는 우리에게 다음 단계로 나아갈 수 있는 직관을 제공합니다.
점수는 평균이 0이고 표준 편차가 1인 표준 정규 분포를 사용하여 무작위로 생성될 수 있습니다. 그러나 우리는 불량품과 불량품에 대해 서로 다른 예측 점수를 원합니다. 우리는 또한 나쁜 점수가 좋은 점수보다 높아야 한다고 원합니다. 따라서 우리는 표준 정규 분포를 사용하고 그 평균을 이동하여 좋은 것과 나쁜 것을 분리합니다.
# scores for bads bads = np.random.normal(0, 1, actual.sum()) + 1 # scores for goods goods = np.random.normal(0, 1, len(actual) - actual.sum()) - 1 plt.hist(bads) plt.hist(goods)
앞서 언급한 코드에서는 두 개의 서로 다른 표준 정규 분포에서 불량 점수와 양호 점수를 샘플링했지만 둘 사이를 분리하기 위해 이동했습니다. 불량 점수(이미지에서 파란색으로 표시)를 오른쪽으로 1만큼 이동하고 그 반대로 왼쪽으로 1만큼 이동합니다. 이를 통해 다음이 보장됩니다.
물론 'shift' 매개변수를 늘리고 1보다 큰 값을 할당하여 이러한 분리를 최대화할 수 있습니다. 하지만 이 이야기에서는 그렇게 하지 않겠습니다. 이에 대해서는 이어지는 관련 이야기에서 살펴보겠습니다. 이제 이 점수에 의해 생성된 확률을 살펴보겠습니다.
# prob for bads bads_prob = list((map(lambda x: 1/(1 + math.exp(-x)), bads))) # prob for goods goods_prob = list((map(lambda x: 1/(1 + math.exp(-x)), goods))) plt.hist(bads_prob) plt.hist(goods_prob)
앞에서 설명한 것처럼 '점수'가 로지스틱 기능을 통해 푸시되면 확률을 얻습니다. 나쁜 확률(파란색)이 좋은 확률(주황색)(0쪽으로 치우침)보다 더 높고(1쪽으로 치우쳐 있음) 분명합니다.
다음 단계는 분석을 위해 실제 벡터와 예측 벡터를 하나의 단일 데이터 프레임으로 결합하는 것입니다. 데이터 인스턴스가 실제로 나쁜 경우 나쁜 확률을 할당하고 그 반대의 경우도 마찬가지입니다.
# create predicted array bads = 0 goods = 0 predicted = np.zeros((10000)) for idx in range(0, len(actual)): if actual[idx] == 1: predicted[idx] = bads_prob[bads] bads += 1 else: predicted[idx] = goods_prob[goods] goods += 1 actual_df = pd.DataFrame(actual, columns=['actual']) predicted_df = pd.DataFrame(predicted, columns=['predicted']) predicted_df = pd.concat([actual_df, predicted_df], axis = 1) predicted_df = predicted_df.sort_values(['predicted'], ascending = False).reset_index() predicted_df = predicted_df.drop(columns = 'predicted')
다음 단계는 저장소를 만드는 것입니다. 이는 우리가 생성하려는 곡선이 본질적으로 불연속적이기 때문입니다. 각 빈에 대해 원하는 측정항목을 누적하여 계산합니다. 즉, 우리는 이산 확률 변수(좋은 것과 나쁜 것)에 대한 누적 분포 함수를 생성합니다.
n_bins = 50 bin_size = math.floor(len(predicted_df) / n_bins) curve_metrics = []
우리는 참고용으로 불량품 및 물품의 양과 상자 크기를 수집해야 합니다.
print("number of bads:", bads) print("number of goods:", goods) print("number of total data points:", len(actual)) print("bin size:", len(predicted_df) / n_bins)
불량 건수: 4915
상품수: 5085
총 데이터 포인트 수: 10000
빈 크기: 200.0
다음은 기본 측정항목을 실제로 계산하는 기본 코드 조각 입니다.
for k in range(1, n_bins + 1): if k < n_bins: TP = predicted_df.loc[ : k*bin_size-1, "actual"].sum() FP = k*bin_size - TP FN = predicted_df.loc[(k*bin_size) : , "actual"].sum() TN = len(actual) - k*bin_size - FN cum_bads = predicted_df.loc[ : k*bin_size-1, "actual"].sum() cum_goods = k*bin_size - cum_bads else: TP = predicted_df.loc[ : , "actual"].sum() FP = len(actual) - TP FN = 0 TN = 0 cum_bads = bads cum_goods = goods curve_metrics.append([k, TP, FP, TN, FN, cum_bads, cum_goods])
for 루프는 1부터 n_bins까지 실행됩니다. 즉, 끝에 1이 남습니다. 따라서 '중지 값'으로 n_bins + 1이 있습니다.
k = 1 ~ k = n_bins-1의 경우 bin_size를 사용하여 참양성, 거짓양성, 거짓음성, 참음성, 누적 bd 및 누적 상품에 대한 누적 계산을 수행합니다.
"predicted_df.loc[ : k* bin_size-1, "actual"].sum()" 조각은 index = 0에서 index = kbin_size-1까지 실행됩니다. 따라서 k *bin_size와 동일한 청크를 꺼냅니다 . 따라서 k *bin_size에서 1을 뺍니다.
마찬가지로, "predicted_df.loc[(k*bin_size) : , "actual"].sum()" 스니펫은 index = k*bin_size에서 최종 인덱스까지 실행됩니다. 따라서 bin이 0에서 49(크기 50)인 경우 스니퍼는 index = 50(bin_size와 동일)부터 계속 실행됩니다.
k = n_bins의 경우 데이터 세트의 최종 인덱스로 확장합니다. 여기서 "predicted_df.loc[ : , "actual"].sum()" 조각은 인덱싱이 인덱스 = 0부터 데이터 프레임의 최종 인덱스까지 실행될 때 모든 잘못된 점을 요약합니다. "TP = bads"로 바꿀 수도 있습니다. FN과 TN은 모두 = 0입니다. 왜냐하면 마지막 컷오프에서 모든 것을 '불량'으로 할당하기 때문입니다. 따라서 False Negative(실제 불량) 또는 True Negative(실제 좋음)가 남지 않습니다. 왜냐하면 k = n_bins이면 네거티브 클래스가 존재하지 않기 때문입니다.
누적 행렬이 어떤 모양인지 확인하는 것이 유용합니다.
curve_metrics
k = n_bins = 50의 경우 모든 좋은 것(5085)과 모든 나쁜 것(4915)을 누적했습니다.
이제 원하는 곡선에 필요한 실제 계산을 할 준비가 되었습니다.
curve_metrics_df = pd.DataFrame(curve_metrics, columns=["cut_off_index", "TP", "FP", "TN", "FN", "cum_bads", "cum_goods"]) curve_metrics_df["cum%bads"] = curve_metrics_df["cum_bads"] / (actual.sum()) curve_metrics_df["cum%goods"] = curve_metrics_df["cum_goods"] / (len(actual) - actual.sum()) curve_metrics_df["precision"] = curve_metrics_df["TP"] / (curve_metrics_df["TP"] + curve_metrics_df["FP"]) curve_metrics_df["recall"] = curve_metrics_df["TP"] / (curve_metrics_df["TP"] + curve_metrics_df["FN"]) curve_metrics_df["sensitivity"] = curve_metrics_df["TP"] / (curve_metrics_df["TP"] + curve_metrics_df["FN"]) # specificity is the recall on the negative class curve_metrics_df["specificity"] = curve_metrics_df["TN"] / (curve_metrics_df["TN"] + curve_metrics_df["FP"])
그게 다야. 이제 곡선을 그리는 데 필요한 모든 것이 준비되었습니다.
plt.plot(curve_metrics_df["cum%goods"], curve_metrics_df["cum%bads"], label ="roc curve") plt.xlabel("cum%goods") plt.ylabel("cum%bads") plt.title("ROC Curve") plt.legend() plt.show() plt.plot(1 - curve_metrics_df["specificity"], curve_metrics_df["sensitivity"], label ="sensitivity specificity curve") plt.xlabel("1 - Specificity") plt.ylabel("Sensitivity") plt.title("Sensitivity vs 1-Specificity Curve") plt.legend() plt.show() plt.plot(curve_metrics_df["recall"], curve_metrics_df["precision"], label ="precision recall curve") plt.xlabel("Precision") plt.ylabel("Recall") plt.title("Precision Recall Curve") plt.legend() plt.show()
모든 곡선은 고도로 숙련된 모델의 사용을 확인합니다(처음에 공식화한 대로). 이렇게 하면 처음부터 이러한 곡선을 만드는 작업이 완료됩니다.
생각할 거리 - "클래스의 불균형이 심각할 때 이 곡선은 어떻게 되나요?" 이것이 다음 이야기의 주제가 될 것입니다.
이 내용이 마음에 드신다면 제 다른 이야기도 꼭 읽어보시기 바랍니다.