به تصویر کشیدن نزول گرادیانی با استفاده از پایتون

رگرسیون خطی عموماً اولین فصل از یادگیری ماشین (Machine Learning) است که آموخته می شود و نزول گرادیانی (Gradient Descent) احتمالاً اولین تکنیک بهینه سازی است که هر کسی می آموزد. در این مطلب سعی داریم که با استفاده از پایتون (Python) این روش را به تصویر بکشیم.

رسم کانتور

تفاوت رسم کانتوری با رسم سه بعدی در این است که رسم کانتور یک رسم دو بعدی است که مقدار بعد سوم با استفاده از کانتورها (خطوط برچسب گذاری شده یا رنگ ها) مشخص شده است. در تصویر زیر یک نمونه رسم کانتوری و رسم سه بعدی در کنار یکدیگر نمایش داده شده اند:

رسم کانتور با استفاده از پایتون

قبل از شروع ورود به نزول گرادیانی، اجازه دهید انتخاب کنیم که با چه روشی در پایتون کانتورها را رسم خواهیم نمود. ما برای این کار از کتابخانه (Library) محبوب matplotlib استفاده خواهیم نمود.

تهیه داده ها

با استفاده از تابع np.linspace دو بردار می سازیم:

import numpy as np
import matplotlib.pyplot as plt
  
x1 = np.linspace(-10.0, 10.0, 100)
x2 = np.linspace(-10.0, 10.0, 100)

در صورتی که نمودار ساده پراکندگی x1 و x2 را رسم کنیم، شکل زیر نمایان می گردد.

plt.scatter(x1, x2)
plt.show()

حال برای ساخت یک رسم کانتوری از np.meshgrid استفاده می کنیم تا دو بردار x1 و x2 را به یک ماتریس 100 در 100 تبدیل کنیم. اجازه دهید دقیق تر به طرز کار این تابع توجه کنیم. این تابع دو ورودی دارد و حاصل آن دو ماتریس است. به عنوان نمونه به کد زیر دقت کنید:

a=np.array((1,2,3))
a1,a2=np.meshgrid(a,a)

همان گونه که در زیر نمایش داده شده است در ماتریس a1 بردار a به صورت سطری تکرار شده است و در ماتریس a2 بردار a به صورت ستونی تکرار شده است.

a1
Out[11]: 
array([[1, 2, 3],
       [1, 2, 3],
       [1, 2, 3]])
a2
Out[12]: 
array([[1, 1, 1],
       [2, 2, 2],
       [3, 3, 3]])

حال فرض می کنیم y تابعی از x1 وx2 به شکل زیر است:

Y=X_1^2+X_2^2
X1, X2 = np.meshgrid(x1, x2)
Y = np.sqrt(np.square(X1) + np.square(X2))

قبل از این که کانتورها را بسازیم اگر نمودار X1 و X2 با رنگ Y را رسم کنیم، شکل زیر ساخته می شود:

cm = plt.cm.get_cmap('viridis')
plt.scatter(X1, X2, c=Y, cmap=cm)
plt.show()

تابع contour و contourf

ما از دو تابع کتابخانه matplotlib به نام های contour و contourf برای رسم خطوط کانتور استفاده می کنیم. ورودی این تابع سه ماتریس است.

cp = plt.contour(X1, X2, Y)
plt.clabel(cp, inline=1, fontsize=10)
plt.xlabel('X1')
plt.ylabel('X2')
plt.show()

همانگونه که مشاهده می شود نمودار پراکندگی و نمودار کانتور هر دو یک رنگ دارند. با این حال طرح کانتور به نسبت نمودار پراکندگی نمای قابل کنترل تری دارد.

رنگ کردن سطح نمودار کانتور

تابع contourf را می توان برای رنگ کردن زمینه نمودار کانتور استفاده نمود. برای تنظیمات دقیق تر این توابع می توان به اسناد توسعه دهندگان matplotlib مراجعه نمود (لینک).

cp = plt.contour(X1, X2, Y, colors='black', linestyles='dashed', linewidths=1)
plt.clabel(cp, inline=1, fontsize=10)
cp = plt.contourf(X1, X2, Y, )
plt.xlabel('X1')
plt.ylabel('X2')
plt.show()

تعیین خط ترازهای دلخواه

در شکل بالا به صورت پیش فرض خط هایی برای تراز کانتورها انتخاب شده اند. با استفاده از چهارمین پارامتر تابع contour، می توان این خطوط را تغییر داد. در کد زیر با تعریف level این کار صورت پذیرفته است.

levels = [0.0, 1.0, 2.0, 4.0, 8.0, 12.0, 14.0]
cp = plt.contour(X1, X2, Y, levels, colors='black', linestyles='dashed', linewidths=1)
plt.clabel(cp, inline=1, fontsize=10)
cp = plt.contourf(X1, X2, Y, levels)
plt.xlabel('X1')
plt.ylabel('X2')
plt.show()

الگوریتم نزول گرادیانی

  • برای نمونه از داده های تبلیغات استفاده نموده ایم.
  • از کتابخانه pandas برای خواندن داده ها استفاده می گردد.
  • میزان فروش متغیر هدف ماست.
  • TV و Radio پیش بینی کننده های ما هستند.
  • برای نرمالایز کردن متغیرها از StandardScaler استفاده می نماییم.
import pandas as pd
 
data = pd.read_csv('http://www-bcf.usc.edu/~gareth/ISL/Advertising.csv')
y = data['sales']
X = np.column_stack((data['TV'], data['radio']))
 
from sklearn.preprocessing import StandardScaler
 
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

محاسبه گرادیان و خطای میانگین مربعات(MSE)

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

def gradient_descent(W, x, y):
    y_hat = x.dot(W).flatten()
    error = (y - y_hat)
    mse = (1.0 / len(x)) * np.sum(np.square(error))
    gradient = -(1.0 / len(x)) * error.dot(x)
    return gradient, mse

سپس نقطه ابتدایی، نرخ یادگیری و تلورانس همگرایی را تعیین می نماییم. همچنین دو آرایه برای ذخیره مقادیر گرادیان و خطای میانگین مربعات می سازیم.

w = np.array((-40, -40))
alpha = .1
tolerance = 1e-3
 
old_w = []
errors = []

نزول گرادیانی

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

# Perform Gradient Descent
iterations = 1
for i in range(200):
    gradient, error = gradient_descent(w, X_scaled, y)
    new_w = w - alpha * gradient
 
    # Print error every 10 iterations
    if iterations % 10 == 0:
        print("Iteration: %d - Error: %.4f" % (iterations, error))
        old_w.append(new_w)
        errors.append(error)
 
    # Stopping Condition
    if np.sum(abs(new_w - w)) < tolerance:
        print('Gradient Descent has converged')
        break
 
    iterations += 1
    w = new_w
 
print('w =', w)

در انتها مقدار w به شکل زیر همگرا می گردد.

w
Out[19]: array([3.91359776, 2.77964408])

ابتدا old_w را به آرایه ای از نوع آرایه های numpy تبدیل می نماییم. سپس پنج مقدار برای کانتورها می سازیم. سپس مقادیر مرتب شده errors را در بردار levels ذخیره می کنیم.

all_ws = np.array(old_w)
 
# Just for visualization
errors.append(600)
errors.append(500)
errors.append(400)
errors.append(300)
errors.append(225)
 
levels = np.sort(np.array(errors))

رسم کانتور

این شکلی است که ما قصد داریم در انتها رسم نماییم.

ساختن محور

در مثال ابتدای متن ما مقادیر x1 و x2 را به صورت تصادفی ساختیم اما اینجا با توجه به مقادیر ورودی می توانیم محورها را رسم کنیم. در کد زیر یک ماتریس برای مقادیر میانگین مربعات ساخته ایم.

w0 = np.linspace(-w[0] * 5, w[0] * 5, 100)
w1 = np.linspace(-w[1] * 5, w[1] * 5, 100)
mse_vals = np.zeros(shape=(w0.size, w1.size))

در مثال ابتدایی، ما مقدار تابع هدف را با یک تابع فرضی ساختیم حال آنکه در این جا با توجه به مقادیر w0 و w1 باید مقدار میانگین مربعات خطا را به دست آوریم.

ساختن بعد سوم

اگر برای هر یک از مقادیر w0 و w1 مقدار MSE (میانگین مربعات خطا یا Mean Squared Error) را محاسبه کنیم، یک ماتریس 100 در 100 برای آن خواهیم داشت.

for i, value1 in enumerate(w0):
    for j, value2 in enumerate(w1):
        w_temp = np.array((value1,value2))        
        mse_vals[i, j] = gradient_descent(w_temp, X_scaled, y)[1]

رسم نهایی

از آن جایی که قبلاً مقادیر w و MSE را ساخته ایم، رسم نمودار کار خیلی سختی نخواهد بود.

  • ابتدا با استفاده از contourf نمودار پراکندگی را رسم می کنیم.
  • دو محور مبدأ برای w های برابر صفر رسم می کنیم.
  • با استفاده از تابع annotate در یک حلقه، مقادیر MSE و w ذخیره شده را فراخوانی کرده و روند حرکت به سوی همگرایی را رسم می کنیم.
  • با فراخوانی تابع contour خطوط کانتور را رسم می نماییم.
plt.contourf(w0, w1, mse_vals, levels,alpha=.7)
plt.axhline(0, color='black', alpha=.5, dashes=[2, 4],linewidth=1)
plt.axvline(0, color='black', alpha=0.5, dashes=[2, 4],linewidth=1)
for i in range(len(old_w) - 1):
    plt.annotate('', xy=all_ws[i + 1, :], xytext=all_ws[i, :],
                 arrowprops={'arrowstyle': '->', 'color': 'r', 'lw': 1},
                 va='center', ha='center')
 
CS = plt.contour(w0, w1, mse_vals, levels, linewidths=1,colors='black')
plt.clabel(CS, inline=1, fontsize=8)
plt.title("Contour Plot of Gradient Descent")
plt.xlabel("w0")
plt.ylabel("w1")
plt.show()

نتیجه گیری

با توجه به مقادیر MSE که کاهش یافته اند 732 -> 256 -> 205 -> ... می توان به صحت نزول گرادیانی پی برد. امیدوارم با استفاده از این توابع بتوانید منحنی های پیچیده تری را در پایتون رسم نمایید.