رگرسیون لجستیک در پایتون

طرح مسأله

فرض کنید قصد دارید برای انتخاب شدن یا نشدن دانشجویان در دانشگاه نمرات دو امتحان را معیار قرار دهید. آیا می توان برای این دسته بندی از رگرسیون خطی (Linear Regression) استفاده نمود؟ اجازه دهید بررسی کنیم.

رگرسیون لجستیک چیست؟

اگر به خاطر داشته باشید، رگرسیون خطی برای تعیین مقدار متغیرهای پیوسته کاربرد دارد. از رگرسیون لجستیک به طور کلی برای مسائل دسته بندی استفاده می گردد. بر خلاف رگرسیون خطی، در این مسائل متغیر وابسته شامل مقادیر محدودی می شود(مثلاً برنده یا بازنده). مسائلی که متغیر وابسته در آن ها فقط دو مقدار محتمل دارد را رگرسیون لجستیک دودوئی (Binary Logistic Regression) می نامیم. اجازه دهید ببینیم که رگرسیون لجستیک چگونه به مسائل دسته بندی کمک می کند.

در رگرسیون خطی، خروجی حاصل برازش تابعی خطی بر روی مجموع وزن دهی شده چندین ورودی است. در رگرسیون لجستیک ما از تابعی استفاده می کنیم که چنین ترکیب وزن دهی شده از ورودی ها را در نهایت به عددی بین صفر و یک تصویر کند. اگر این تابع وارد محاسبات نگردد خروجی می تواند عددی بیش از یک باشد که برای ما مطلوب نیست. لذا نمی توان از رگرسیون خطی برای مسائل دسته بندی استفاده نمود.

از شکل زیر پیداست که در رگرسیون لجستیک، ترکیب خطی ورودی ها از یک تابع عبور می کند که قادر است تمامی مقادیر حقیقی را به عددی بین صفر و یک تبدیل کند.

مقایسه تصویری پروسه رگرسیون خطی و لجستیک

یکی از توابعی که می تواند چنین رفتاری را شبیه سازی کند، تابع سیگموئید (sigmoid) یا لجستیک نام دارد و شکل آن در ادامه آمده است.

تابع سیگموئید یا لجستیک. منحنی S شکل بالا در ریاضیات به منحنی لجستیک مشهور است.

همان گونه که مشاهده می گردد، مقدار تابع سیگموئید همواره بین صفر و یک است. مقدار خروجی تابع را می توان به عنوان احتمال وقوع y=1 تلقی نمود. مثلاً وقتی x=0 است، احتمال وقوع y=1 پنجاه درصد است. با افزایش x این احتمال افزایش و با کاهش آن نیز این احتمال کاهش می یابد.

قبل از ساخت مدل، پیش فرض های رگرسیون لجستیک را مرور می کنیم:

  • متغیر وابسته باید مقادیر مطلق و منفصل داشته باشد.
  • متغیرهای توصیفی باید نسبت به یکدیگر مستقل باشند.

مجموعه داده

داده ها از دوره یادگیری ماشین (Machine Learning) اندرو ان جی در سایت Coursera استخراج گشته اند که قابل دانلود کردن از این لینک هستند. این مجموعه داده شامل نمرات صد داوطلب در دو امتحان است. مقدار هدف، عدد صفر یا یک است. یک به معنی پذیرش در دانشگاه و صفر به معنی عدم پذیرش. هدف ساخت یک مدل پیش طبقه بندی کننده دودوئی است که بتواند پذیرش یا عدم پذیرش یک داوطلب را پیش بینی نماید.

ابتدا داده ها را با استفاده از تابع read_csv در pandas بارگذاری می نماییم. همچنین داده ها را به دو دسته پذیرفته شده و پذیرفته نشده تقسیم می کنیم.

# imports
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd


def load_data(path, header):
    marks_df = pd.read_csv(path, header=header)
    return marks_df


if __name__ == "__main__":
    # load the data from the file
    data = load_data("data/marks.txt", None)

    # X = feature values, all the columns except the last column
    X = data.iloc[:, :-1]

    # y = target values, last column of the data frame
    y = data.iloc[:, -1]

    # filter out the applicants that got admitted
    admitted = data.loc[y == 1]

    # filter out the applicants that din't get admission
    not_admitted = data.loc[y == 0]

    # plots
    plt.scatter(admitted.iloc[:, 0], admitted.iloc[:, 1], s=10, label='Admitted')
    plt.scatter(not_admitted.iloc[:, 0], not_admitted.iloc[:, 1], s=10, label='Not Admitted')
    plt.legend()
    plt.show()
نمایی از داده های ورودی (نمرات دو امتحان) جهت آموزش مدل رگرسیون لجستیک

حال که داده ها و مسأله شفاف شده است، می توانیم به آموزش مدل بپردازیم.

تابع هزینه و فرضیه

از آن جا که رگرسیون لجستیک را درک نموده ایم، در این قسمت می توانیم تابع هزینه و فرضیه را تعریف نماییم. یک مدل رگرسیون خطی را می توان با معادله زیر نمایش داد:

h(x)=\theta^Tx

حال خروجی رگرسیون خطی را به عنوان ورودی تابع سیگموئید استفاده می کنیم:

h(x)=\sigma(\theta^Tx)

که در آن تابع سیگموئید به شکل زیر تعریف شده است:

\sigma(t)=\frac {1}{1+e^{-t}}

در نتیجه تابع فرضیه رگرسیون لجستیک به شکل زیر تعریف می گردد:

h(x)=\frac {1}{1+e^{-\theta^Tx}} h(x)=\begin{cases} >0.5 \quad if\ \theta^Tx>0\\ <0.5\quad if \ \theta^Tx<0\\ \end{cases}

تابع هزینه

مانند رگرسیون خطی این جا هم یک تابع هزینه تعریف می کنیم و سعی داریم آن را کمینه کنیم:

cost=\begin{cases} -log(h(x)) \quad if\ y=1\\ -log(1-h(x)) \quad if \ y=0\\ \end{cases}

تابع هزینه به شکل شهودی

اگر به فرم تابع هزینه دقت کنیم، در مواقعی که مقدار تابع فرضیه با کلاس قطعی (y) همسان باشد تابع هزینه صفر و در غیر این صورت بسته به میزان فاصله مقدار پیش بینی شده توسط تابع فرضیه با کلاس واقعی، عدد بیشتری به تابع هزینه افزوده خواهد شد. در گراف زیر این امر به خوبی قابل مشاهده است.

نمای شهودی تابع فرضیه (عناصر شکل دهنده تابع هزینه)

دو ضابطه را می توان به شکل زیر در یک جمله جمع بندی نمود:

cost(h(x),y)=-y log(h(x))-(1-y)log(1-h(x))

با میانگین گیری از مقدار تابع هزینه برای تمامی نمونه ها (m تعداد نمونه هاست) تابع هزینه کل داده ها به شکل زیر محاسبه می گردد:

J(\theta)=\frac 1 m \sum_{i=1}^m [y^i log(h(x^i))+(1-y^i)log(1-h(x^i))]

ما برای کمینه کردن تابع هزینه از روش نزول گرادیانی استفاده می کنیم. در نتیجه گرادیان به شکل زیر خواهد بود:

\frac {\partial J(\theta)}{\partial \theta_j}=\frac 1 m \sum_{i=1}^m (h(x^i)-y^i)x^i_j

این معادله شبیه معادله ای است که برای رگرسیون خطی نیز به آن رسیده بودیم. فقط در این جا تابع فرضیه متفاوت است.

آموزش مدل

حال هر چیزی که برای پیاده سازی پروسه آموزش مدل نیاز داریم را آماده کرده ایم. در ابتدا ماتریس های مناسبی در حافظه برای داده ها (X و y) و ضرایب theta (که در نهایت خروجی آموزش خواهند بود) تعریف می کنیم.

X = np.c_[np.ones((X.shape[0], 1)), X]
y = y[:, np.newaxis]
theta = np.zeros((X.shape[1], 1))

و در ادامه توابعی که هزینه را محاسبه می کنند را تعریف می کنیم.

def sigmoid(x):
    # Activation function used to map any real value between 0 and 1
    return 1 / (1 + np.exp(-x))

def net_input(theta, x):
    # Computes the weighted sum of inputs
    return np.dot(x, theta)

def probability(theta, x):
    # Returns the probability after passing through sigmoid
    return sigmoid(net_input(theta, x))

سپس تابع هزینه (cost_function) و گرادیان (gradient) را تعریف می کنیم.

def cost_function(self, theta, x, y):
    # Computes the cost function for all the training samples
    m = x.shape[0]
    total_cost = -(1 / m) * np.sum(
        y * np.log(probability(theta, x)) + (1 - y) * np.log(
            1 - probability(theta, x)))
    return total_cost

def gradient(self, theta, x, y):
    # Computes the gradient of the cost function at the point theta
    m = x.shape[0]
    return (1 / m) * np.dot(x.T, sigmoid(net_input(theta,   x)) - y)

همچنین تابع fit را برای کمینه کردن تابع هزینه تعریف می کنیم. در این جا ما از تابع fmin_tnc از کتابخانه scipy استفاده کرده ایم.

def fit(self, x, y, theta):
    opt_weights = fmin_tnc(func=cost_function, x0=theta,
                  fprime=gradient,args=(x, y.flatten()))
    return opt_weights[0]
parameters = fit(X, y, theta)

پارامترهای آموزش دیده نهایی مدل عبارت اند از:
 [-25.16131856 0.20623159 0.20147149]

برای این که متوجه شویم مدل ما چقدر مناسب است، مرز تصمیم گیری را رسم می کنیم.

رسم مرز تصمیم گیری

از آن جا که دو ورودی برای مدل ما وجود دارد، می توان معادله را به شکل زیر نوشت.

h(x)=\theta_0+\theta_1x_1+\theta_2x_2

مرز تصمیم گیری می تواند به کمک معادله زیر رسم گردد.

x_2=- \frac {\theta_0+\theta_1x_1} {\theta_2}

مرز تصمیم گیری را بر روی نمودار نمایش داده های خام آموزش رسم می کنیم.

x_values = [np.min(X[:, 1] - 5), np.max(X[:, 2] + 5)]
y_values = - (parameters[0] + np.dot(parameters[1], x_values)) / parameters[2]

plt.plot(x_values, y_values, label='Decision Boundary')
plt.xlabel('Marks in 1st Exam')
plt.ylabel('Marks in 2nd Exam')
plt.legend()
plt.show()
مرز تصمیم گیری حاصل از آموزش مدل لجستیک با دقت خوبی توانسته داده ها رو به دو گروه طبقه بندی کند.

همان طور که مشهود است، مدل ما در پیش بینی کلاس نتیجه مناسبی داشته است. اما مدل ما به طور کمّی در چه حدی دقت دارد؟

دقت مدل

def predict(self, x):
    theta = parameters[:, np.newaxis]
    return probability(theta, x)
def accuracy(self, x, actual_classes, probab_threshold=0.5):
    predicted_classes = (predict(x) >= 
                         probab_threshold).astype(int)
    predicted_classes = predicted_classes.flatten()
    accuracy = np.mean(predicted_classes == actual_classes)
    return accuracy * 100
accuracy(X, y.flatten())

دقت مدل ۸۹ درصد است. حال با استفاده از scikit-learn همین کار را تکرار کرده و نتایج را با پروسه آموزشی که خودمان کدنویسی کردیم، مقایسه می کنیم.

پیاده سازی scikit-learn

from sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import accuracy_score 
model = LogisticRegression()
model.fit(X, y)
predicted_classes = model.predict(X)
accuracy = accuracy_score(y.flatten(),predicted_classes)
parameters = model.coef_

پارامترهای مدل عبارت اند از [[-2.85831439, 0.05214733, 0.04531467]] و دقت آن هم ۹۱ درصد است.

اما چرا نتایج مدل ما با مدل scikit-learn متفاوت است؟ دلیل آن بحث انجام یا عدم انجام رگولاریزاسیون (regularization) است که ما از آن استفاده نکردیم. اصولاً این کار برای جلوگیری از بیش برازش (overfitting) است که در مطالب آتی به طور مفصل به آن اشاره خواهد شد.