هوش مصنوعی (Artificial Intelligence) به ماشینهایی دلالت دارد که میتوانند مانند انسان یا حیوانات یاد بگیرند، استدلال کنند، خودشان اقدام کنند و تصمیم بگیرند. امروزه تحقیقات در حوزه هوش مصنوعی تلاشهای متنوعی را در حوزههای بینایی ماشین، پردازش زبان طبیعی، رباتیک و یادگیری ماشین شامل میشود.
گرچه امروزه حوزه هوش مصنوعی یکی از پیشرانهای حوزه فنآوری محسوب میشود، بدان معنی نیست که این تلاشها بهتازگی صورت پذیرفته است. مطالعه این تلاشها و افکاری که پشت توسعه الگوریتمها است به خواننده کمک میکند منطق توسعه این الگوریتمها را بهتر درک کند و با چالشهای هوش مصنوعی و مدلهای یادگیری ماشین بهتر آشنا شود. در این مقاله من به اولین مدل هوش مصنوعی موسوم به پرسپترون که در دهه ۱۹۵۰ میلادی توسعه یافت، میپردازم و آن را در پایتون پیادهسازی میکنم.
مغر انسان؛ الهامبخش هوش مصنوعی
مغز انسان یادگیرنده شگفتانگیزی است. سلولهای عصبی یا همان نورونها (Neurons) ساختار اصلی تشکیلدهنده مغز و سیستم عصبی، هستند. شکل-۱ یک نورون را نشان میدهد. سیگنالهای عصبی از طریق دندریتها وارد هسته سلول میشود. ورودیها در درون هسته سلول تجمیع میشوند و اگر از یک آستانهای (Threshold) بیشتر شوند سیگنال خروجی از طریق آکسان انتقال مییابد.
اولین تلاشها برای مدلسازی ماشینی که نزدیک به مغر انسان عمل کند، مربوط به مقالهای است که مک کالِک (McCulloch) و پیتس (Pitts) در سال ۱۹۴۳ منتشر کردند (شکل-۲). در این مقاله آنها سعی کردند که یک مدل ریاضی مبتنی بر یک دروازه دودویی (Binary Gate) از نورون ارائه کنند.
شکل-۳ عملکرد نورون را به زبان ریاضی توضیح میدهد. ترکیب خطی متغیرهای ورودی وارد نورون میشوند. برای محاسبه این ترکیب خطی، مقادیر ورودی در ضرایب ثابت متناظر ضرب شده و سپس همه این مقادیر با یکدیگر جمع میشوند. درنهایت پس از اضافه شدن یک عدد ثابت که سوگیری (Bias) نام دارد، خروجی () محاسبه میشود:
در گام بعدی، وارد تابع فعالسازی (Activation Function) میشود. تابع فعالسازی تصمیم میگیرد که نورون فعال شود (خروجی ۱ بدهد) یا غیرفعال بماند (خروجی صفر بدهد). در آن زمان تابع فعالسازی پلهای (Step Function) پیشنهاد گردید. در تابع پلهای اگر ورودی از عدد صفر بزرگتر یا مساوی باشد، خروجی ۱ و اگر کوچکتر از صفر باشد، خروجی صفر است.
پرسپترون چگونه یاد میگیرد؟
بهطور خلاصه، پرسپترون مجموعهای از سیگنالهای ورودی را دریافت میکند و اگر ترکیب خطی این ورودیها از مقدار آستانه بیشتر شد فعال میشود، وگرنه غیرفعال باقی میماند.
برای آنکه پرسپترون کار کند، نیاز است تا ضرایب و سوگیری برای آن مسئله مشخص معلوم باشند. اینها پارامترهای مدل هستند که باید محاسبه شوند. پرسش این است این پارامترها چگونه باید محاسبه شوند؟
فرانک روزنبلات (Frank Rosenblatt) که تصویرش را در شکل-۴ میبینید، در سال ۱۹۵۷ الگوریتمی پیشنهاد داد که بتواند با مقادیر اولیهای برای پارامترها شروع کند و در گامهای بعدی بهتدریج بر اساس ورودیهای مختلف پارامترهای اولیه را اصلاح و به سمت پارامتر بهینه میل کند. الگوریتم یادگیری پیشنهادی وی، برای حل مسائلی کاربرد داشت که هدف تشخیص تمایز دو رستهای بود که به شکل خطی از هم جداشدنی هستند.
در زبان یادگیری ماشین، به مسائلی که هدف آن پیشبینی یک متغیر رستهای (Categorical Variable) بر اساس مجموعهای از ورودیهاست، مسائل دستهبندی (Classification) گفته میشوند. عملاً پرسپترون یک مسئله دستهبندی باینری را حل میکند.
برای آشنایی بیشتر با انواع الگوریتمهای یادگیری ماشین به مقاله “مقدمهای بر یادگیری ماشین” مراجعه کنید.
الگوریتم پیشنهادی روزنبلات بسیار ساده است و به طریق زیر عمل میکند:
گام اول، پارامترها صفر یا اعداد تصادفی کوچک در نظر گرفته شوند.
گام دوم، به ازای هر مشاهده:
بر اساس ورودی دادهها و پارامترها، خروجی نهایی () پیشبینی شود.
بر اساس رابطه زیر پارامترها بهروزرسانی شوند:
در رابطه بالا، اندیس مشاهده و اندیس متغیر (همان ویژگی (Feature) به زبان یادگیری ماشین) است. نرخ یادگیری (Learning Rate) است که تعیین میکند چه میزان وزنها اصلاح شوند. مقدار واقعی و مقدار پیشبینی رسته مشاهده است. همانطور که از رابطه بالا مشخص است اگر بین مقدار واقعی و پیشبینی تفاوتی نباشد، پارامترها بهروز نمیشوند ولی اگر تفاوت باشد بر اساس میزان نرخ یادگیری مقدار پارامترها بهروز میشوند. نکته دیگر اینکه پارامترها، همگی باهم بهروز میشوند.
اگر همه مشاهدات نمونه، از الگوریتم عبور کند و فرآیند آموزش یکبار روی دادهها طی شود، اصطلاحاً یک اِپُک (Epoch) طی شده است. مدلساز میتواند تعیین کند، الگوریتم به ازای چند اپک اجرا گردد.
همگرا شدن الگوریتم تنها در صورتی تضمین میشود که دو رسته به شکل خطی از هم جداشدنی باشند. اگر نتوان دو رسته را با یک مرز خطی از هم جدا کرد، میتوان تعیین کرد پس از چند اپک الگوریتم متوقف شود.
پیادهسازی پرسپترون در پایتون
برای آنکه درک بهتری از این الگوریتم پیدا کنید، من مثالی را در پایتون شبیهسازی کردم که در آن دو متغیر پیشبینی کننده داریم که بر اساس آن باید رسته مشاهده (آبی یا قرمز) را پیشبینی کنیم.
در این مثال برای ساخت داده مصنوعی (Synthetic Data)، از ماژول datasets در کتابخانه sklearn استفاده کردم. تابع make_blobs اجازه میدهد دادههای خوشهای ایجاد کنیم. آرگومان centers مراکز خوشه و cluster_std پراکندگی حول مراکز خوشه را مشخص میکند. ماتریس ویژگیها را در آرایه features و ماتریس پاسخ را در آرایه response ذخیره کردم. کد زیر نحوه ساخت دادهها را نشان میدهد.
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 29 30 31 32 33 34 | #Required libraries import numpy as np #for numerical computation import matplotlib.pyplot as plt #for data visualization #Create synthetic dataset from sklearn import datasets features, response = datasets.make_blobs(n_samples = [100, 100], n_features = 2, centers = [(5, 5),(8, 8)], cluster_std = 0.5, random_state = 123) features[:5, :] array([[6.04355668, 5.08222062], [5.09051756, 5.58893097], [5.06037368, 5.37410781], [7.81212504, 7.31013751], [7.83807011, 7.90585157]]) response[:5] array([0, 0, 0, 1, 1]) #Plot data cdict = {0: 'red', 1: 'blue'} plt.figure(figsize = (6, 6)) for i in np.unique(response): indices = np.where(response == i) plt.scatter(x = features[indices, 0], y = features[indices, 1], c = cdict[i], label = i, marker = "o", alpha = 0.7) plt.xlabel('X1') plt.ylabel('X2') plt.legend() |
نتیجه کار در شکل-۵ مشخص است. همانطور که پیداست این دو رسته به شکل کاملی از هم جدا شدهاند و مثال مناسبی است برای آنکه بتوان الگوریتم پرسپترون را بر روی آن پیادهسازی کرد.
در کد زیر ابتدا من تابع فعالسازی پلهای را ایجاد کردم و خروجی آن را برای ورودی ۵ و ۲- بررسی کردم:
1 2 3 4 5 6 7 8 9 | #Activation function def activation_func(x): return np.where(x >= 0, 1, 0) print(activation_func(5)) 1 print(activation_func(-2)) 0 |
در گام بعدی، الگوریتم یادگیری پرسپترون را در تابع perceptron_fit پیادهسازی کردم. در کد زیر کاربر ماتریس ویژگی، ماتریس پاسخ، نرخ یادگیری و تعداد اپک را بهعنوان ورودی به تابع میدهد. در گام بعد، مقادیر اولیه پارامترها را صفر در نظر گرفتم. برای بررسی نحوه همگرایی پارامترها دو لیست با عنوان w_history و b_history ایجاد کردم که پس از پایان هر اپک، آخرین مقادیر پارامترها را در خود ذخیره میکنند.
در حلقه اول، به تعداد اپک مراحل یادگیری اجرا میشود. در هر اپک (حلقه دوم) کل دادههای آموزش یکییکی وارد الگوریتم میشوند. ترکیب خطی ورودی با استفاده از تابع dot از کتابخانه Numpy محاسبه شده و در z ذخیره میگردد که خود ورودی تابع activation_func است. در گام بعدی بر اساس همان رابطهای که در بالا توضیح دادم، بهروزرسانی پارامترها صورت میپذیرد. درنهایت تابع پارامترهای برآوردی نهایی و تاریخچه آنان را برمیگرداند.
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 | #Perceptron learing algorithm def perceptron_fit(X, y, learning_rate = 0.01, num_epochs = 100): n_samples, n_features = X.shape #Initialize model parameters weights = np.zeros(n_features) bias = 0 #List to save paramters history w_history = [] b_history = [] #Loop over epochs for i in range(num_epochs): #Loop over train dataset for idx_i, x_i in enumerate(X): z = np.dot(x_i, weights) + bias y_predicted = activation_func(z) #Perceptron update rule update = learning_rate * (y[idx_i] - y_predicted) weights += update * x_i bias += update w_history.append(list(weights)) b_history.append(bias) return([weights, bias], [w_history, b_history]) |
من در کد زیر تابع perceptron_fit را برای برآورد پارامترها در مثالی که ساختم، استفاده کردم:
1 2 3 | res = perceptron_fit(features, response, learning_rate = 0.01, num_epochs = 50) print(res[0]) [array([0.01184069, 0.03086478]), -0.3000000000000001] |
بهاینترتیب،
برآورد شدهاند.
پس از آموزش الگوریتم، از تابع perceptron_predict برای پیشبینی رسته مشاهدات استفاده کردم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #Prediction def perceptron_predict(w, b, X): return(activation_func(np.dot(X, w) + b)) #Prediction on train dataset w = res[0][0] b = res[0][1] y_pred = perceptron_predict(w = w, b = b, X = features) y_pred array([0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0]) #Accuracy from sklearn.metrics import accuracy_score accuracy_score(response, y_pred) * 100 100.0 |
با مقایسه مقدار واقعی و مقدار پیشبینیشده در کل نمونه، میتوان فهمید چند درصد پیشبینیها درست بوده است. این همان شاخص دقت (Accuracy) مدل است. برای این مدل شاخص دقت ۱۰۰ درصد به دست میآید.
برای آنکه درک بهتری از خروجی الگوریتم داشته باشیم، من خط جداکننده را در شکل-۶ رسم کردم. این خط فضای ویژگیها را به دو ناحیه آبی و قرمز تقسیم میکند. در این مثال که رستهها بهخوبی با خط از هم جدا شدهاند عملکرد الگوریتم خیلی خوب است. گرچه بهندرت در مسائل دنیای واقعی با چنین شرایطی مواجه هستیم.
کد رسم شکل بالا را در زیر آوردهام:
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 | #Plot data cdict = {0: 'red', 1: 'blue'} plt.figure(figsize = (6, 6)) for i in np.unique(response): indices = np.where(response == i) plt.scatter(x = features[indices, 0], y = features[indices, 1], c = cdict[i], label = i, marker = "o", alpha = 0.7) plt.xlabel('X1') plt.ylabel('X2') plt.legend() #Plot the classifier x1p = np.linspace(features.min() - 1, features.max() + 1, 100) x2p = (w[0] * x1p + b) / - w[1] plt.plot(x1p, x2p, 'k') plt.xlim(features.min() - 1, features.max() + 1) plt.ylim(features.min() - 1, features.max() + 1) #Plot regions x1, x2 = np.meshgrid(np.linspace(features.min() - 1, features.max() + 1, 100), np.linspace(features.min() - 1, features.max() + 1, 100)) grids = np.array((x1.ravel(), x2.ravel())).T region_color = perceptron_predict(w = w, b = b, X = grids) region_color = region_color.reshape(100, 100) plt.contourf(x1, x2, region_color, alpha = 0.1, levels = [0, 0.5, 1], colors = ['red', 'blue']) |
با توجه به آنکه من تاریخچه تغییرات پارامترها را در w_history و b_history ذخیره کردم، میتوانم نحوه همگرایی آنان را در طول اپکها بررسی کنم. شکل-۷ و شکل-۸ نشان میدهد که آنان خیلی زود همگرا شدند.
حال مثال را کمی چالشی میکنم. در کد زیر من طوری دادههای مصنوعی را ساختم که با خط نتوان بهسادگی رستهها را جدا کرد (شکل-۹). جالب است عملکرد پرسپترون را در این حالت بررسی کنیم.
1 2 3 4 5 6 | #Create Synthetic Dataset from sklearn import datasets features, response = datasets.make_blobs(n_samples = [100, 100], n_features = 2, centers = [(5, 5),(6.5, 5)], cluster_std = 0.5, random_state = 123) |
وقتی از تابع perceptron_fit برای این مثال، به ازای ۵۰ اپک استفاده میکنم، به این پارامترهای برآوردی میرسم:
دقت مدل روی دادههای آموزش به ۸۷ درصد میرسد و خط جداکننده دو رسته در شکل-۱۰ مشخص است.
وقتی تعداد اپک را به ۵۰۰ میرسانم، پارامترهای برآوردی کاملاً تغییر میکنند:
دقت مدل به ۷۸٫۵ درصد میرسد. خط جداکننده حالا مطابق شکل-۱۱ میشود.
به نظر میرسد الگوریتم دچار مشکل شده است. در شکل-۱۲ و شکل-۱۳ تاریخچه تغییرات پارامترها را رسم کردم. همانطور که معلوم است الگوریتم در طول ۵۰۰ اپک نتوانسته همگرا شود. توصیه میکنم الگوریتم را به ازای اپکهای بیشتر و نرخ یادگیری متفاوت اجرا کنید و نتیجه را ببینید.
مثالهایی که حل کردم، نشان داد که پرسپترون، جداکننده خطی است و میتوان آن را برای جدا کردن دو رسته بکار برد. اگر دادهها به شکل خطی قابل جدا کردن نباشد، الگوریتم همگرا نمیگردد.
نکته مهم دیگر آن است که حتی در مثال اولی که دو رسته به شکل خطی از هم جداشدنی هستند، الگوریتم بلافاصله وقتیکه برچسب همه رستهها را درست تشخیص میدهد، متوقف میشود و ضرایب بهروز نمیشوند. این میتواند ما را دچار مشکل تعمیمپذیری کند. بهصورت شهودی، بهتر است خط جداکننده جایی بین دو رسته باشد بهطوریکه حاشیه زیادی با دو طرف داشته باشد تا وقتی مدل در معرض دادههای جدید قرار گرفت، خطای آن کم باشد. نکتهای که الگوریتمهای مدرنتر مانند ماشین بردار پشتیبان (Support Vector Machines – SVMs) به آن توجه میکنند.
در مقابل پرسپترون، تنها تلاش میکند روی دادههایی که برای آموزش استفاده میکند، مرز جداکننده را تشخیص دهد. آنهم با اولین نتیجه خوب، کار را متوقف میکند. شکل-۱۴ تفاوت دو رویکرد را نشان میدهد.
به دلیل نقاط ضعفی که اشاره کردم، امروزه پرسپترون کاربردی ندارد. اما این تلاشهای اولیه گامهایی بودند که ما را وارد عصر هوش مصنوعی کردند.
منابع:
Rosenblatt, F. (1957). “The Perceptron, A Perceiving and Recognizing Automaton Project Para”, Cornell Aeronautical Laboratory
McCulloch W. S. & Pitts W. (1943). “A Logical Calculus of the Ideas Immanent In Nervous Activity”, The Bulletin of Mathematical Biophysics, 5(4):115–۱۳۳