یکی از تکنیکهای مرسوم برای کاهش بعد، PCA یا Principal Component Analysis است. معمولا کار با دادههای حجیم با بعدهای زیاد امکانپذیر نیست و نیاز داریم قبل از انجام امور پردازشی، در صورت امکان، برخی از بعدهایی را که اهمیت زیادی ندارند، حذف کنیم. در این پروژه، به عنوان نمونه، سعی کردهایم، دادههای موجود در ماتریس یک تصویر را مورد بررسی قرار دهیم و در انتها نیز یکی از بعدهای تصویر را کاهش دادهایم. تصویر اولیه، عکس زیر است:
دادههای بسیار حجیم در دنیای امروز، نه تنها چالشی بزرگ برای سختافزارهای محاسباتی به حساب میآیند، بلکه مانعی در پیش روی الگوریتمهای یادگیری ماشین هستند. یکی از روشهایی که برای کاهش حجم و پیچیدگی دادهها استفاده میشود، روش Principal Component Analysis یا PCA است. در آنالیز PCA، هدف ما یافتن الگوهای مختلف در دادههای مورد نظر است؛ به طور دقیقتر، در این آنالیز سعی بر آن است که همبستگی (correlation) میان دادهها به دست آید. اگر در دو یا چند بعد مشخص، بین دادههای ما، همبستگی شدیدی وجود داشته باشد، میتوان آن بعدها را به یک بعد تبدیل نمود؛ به این ترتیب و با کاهش بعد، پیچیدگی و حجم دادهها به شدت کاهش مییابد. اگر بخواهیم هدف غایی آنالیز PCA را بیان کنیم، باید بگوییم: هدف پیدا کردن جهت (بعد) با بیشترین واریانس دادهها و کاهش بعد است؛ به صورتی که کمترین میزان دادهی بااهمیت از دست رود.
در پروژه پردازش تصویر پیش رو، میخواهیم یک تصویر از کاربر دریافت کنیم و پس از انجام پردازشهای مختلف بر روی ماتریس این تصویر، در صورت امکان یکی از بعدهای آن را حذف کنیم؛ به طور واضحتر میخواهیم یکی از رنگهای موجود در تصویر را به گونهای حذف کنیم که کمترین میزان داده از دست رود.
برای انجام این پروژه از ابزارهای زیر استفاده شده است که در زمان استفاده، به توضیح هر یک میپردازیم:
- زبان برنامه نویسی پایتون
- کتابخانه pillow در پایتون
- کتابخانه numpy در پایتون
- کتابخانه matplotlib در پایتون
وارد کردن تصویر به برنامه با استفاده از کتابخانه pillow
برای وارد کردن تصویر به برنامه، از کتابخانهی pillow استفاده میکنیم. این کتابخانه برای انجام امور مربوط به image manipulation مانند تغییر سایز تصویر، چرخش تصویر و … استفاده میشود. در اینجا تنها استفادهی ما از کتابخانهی PIL یا همان pillow وارد کردن تصویر به برنامه است.
1 2 3 4 5 6 7 8 |
from PIL import Image #importing the image / size = 768*1024 filename = "desert.jpg" image = Image.open(filename) #image.size returns a 2-tuple width, height = image.size |
در خط اول، از کتابخانهی pillow، آبجکت Image را وارد میکنیم. سپس با استفاده از متد Image.open و وارد کردن آدرس تصویر بر روی رایانه، تصویر را به شکل یک آبجکت با نام image ذخیره میکنیم. توجه داریم، به علت آن که تصویر در دایرکتوری اصلی برنامه قرار دارد، نیازی به وارد کردن آدرس دقیق آن نیست. در دو خط آخر، با استفاده از متد image.size که یک تاپل شامل طول و عرض تصویر را برمیگرداند، این دو پارامتر را ذخیره میکنیم.
ذخیره تصویر به صورت ماتریس | پیش پردازش تصویر
اطلاعات موجود در یک تصویر را به اشکال گوناگون میتوان نشان داد. یکی از روشهای مرسوم نشان دادن دادههای موجود در تصویر، استفاده از سیستم رنگی RGB است. این سه حرف سرواژهی رنگهای Red (قرمز)، Green (سبز) و Blue (آبی) هستند. همان طور که میدانید تصاویر از واحدهای ریزی به نام پیکسل تشکیل شدهاند. هر پیکسل میتواند همه یا تعدادی از این سه رنگ را با شدتهای مختلف (intensity) داشته باشد. شدت هر رنگ را معمولا با عددی در بازهی ۰ و ۲۵۵ نشان میدهند. این اعداد بیانگر طیفی است که میتوان با حافظه باینری ۸ بیتی ساخت. به عنوان مثال، یک پیکسل میتواند مقادیر زیر را اختیار کند:
$ \small [R, G, B] = [25, 150, 231] $
یک تصویر با توجه به سایزی که دارد، تعداد پیکسلهای متفاوتی دارد. برای ذخیرهسازی عددی یک تصویر، باید مقادیر RGB تمامی پیکسلهای آن را ذخیره کنیم؛ به طور دقیقتر ما نهایتا به ماتریسی دوبعدی، شبیه به ماتریس زیر نیاز داریم:
$ \small [ \begin{bmatrix} 3 & 2 & 241 \end{bmatrix}, \begin{bmatrix} 231 & 150 & 25 \end{bmatrix},…, \begin{bmatrix} 250 & 30 & 12 \end{bmatrix}] $
ماتریس دوبعدی بالا شامل تعدادی ماتریس یک بعدی است. این تعداد در واقع همان تعداد پیکسلهاست؛ به عنوان مثال در یک تصویر با اندازهی ۱۰۲۴*۷۶۸ پیکسل، ۷۸۶۴۳۲ پیکسل داریم؛ به عبارتی دیگر، برای نشان دادن عددی این تصویر، نیازمند ماتریسی دوبعدی هستیم که درون آن ۷۸۶۴۳۲ ماتریس یک بعدی قرار دارد. هر یک از این ماتریسهای یک بعدی، سه عدد اسکالر دارند که بیانگر RGB یک پیکسل است.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import numpy as np #transforming the "image" list into ndarray object "npimage" npimage = np.array(image) #define an array in a desired form (section 7.5 of the book) / [X1 X2 ... Xn] that Xi represents a [x1, x2, x3] for [R, G, B] arr = [] #copying the im array into arr in the desired form for y in range(height - 1): for x in range(width - 1): arr.append(npimage[y, x]) #transforming the "arr" list into ndarray object "nparr" nparr = np.array(arr) |
برای کار با ماتریسها در پایتون از کتابخانهی numpy که به همین منظور ساخته شده است، استفاده میکنیم. این کتابخانه را در خط اول کد بالا و با نام اختصاری np اضافه کردهایم. کتابخانه نامپای حاوی یک آبجکت مهم با نام ndarray است. با استفاده از متد np.array میتوان یک آبجکت ndarray ساخت. ورودی این متد یک لیست، تاپل یا هر شیء آرایهمانند دیگری در پایتون است. ما تصویری را که به عنوان ورودی دریافت کرده بودیم، به شکل یک ndarray و با نام npimage ذخیره میکنیم. ماتریس حاصل شده، مطابق فرم دلخواه ما نیست؛ این ماتریس در حقیقت یک ماتریس سه بعدی است؛ داخل این ماتریس سه بعدی، به تعداد پیکسلهای عمودی تصویر، آرایه دو بعدی وجود دارد؛ در هر یک از این آرایههای دو بعدی، به تعداد پیکسلهای افقی تصویر، آرایه یک بعدی وجود دارد؛ این آرایههای یک بعدی حاوی اعداد اسکالر RGB هستند. برای روشن شدن موضوع، تصور کنید یک تصویر با ۳ پیکسل عمودی و دو پیکسل افقی داریم؛ ماتریس حاصل به شکل زیر خواهد بود:
$ \small \left. [ [ [25, 120, 251], [34, 12, 78] ], [ [243, 30, 80], \right. $
$ \small \left. [25, 150, 231] ], [ [12, 30, 250], [241, 2, 3] ] ] \right. $
همان طور که در قسمت قبلی اشاره شد، فرم دلخواه ما چنین فرمی نیست؛ بنابراین نیاز داریم تا فرم ماتریس را به شکل دلخواه درآوریم. فرم مطلوب ما به شکل زیر است:
$ \small [ \begin{bmatrix} 12 & 30 & 250 \end{bmatrix}, \begin{bmatrix} 25 & 150 & 231 \end{bmatrix},…, \begin{bmatrix} 241 & 2 & 3 \end{bmatrix}] $
برای این کار یک لیست جدید به نام arr ایجاد کردهایم و با استفاده از حلقههای for، آن را پر میکنیم. در نهایت لیست arr را با استفاده از متد np.array به یک آبجکت ndarray با نام nparr تبدیل میکنیم. علت این کار، انجام محاسبات بعدی به صورت بهینه و با سرعت مناسب با استفاده از کتابخانه numpy است.
ترسیم دادهها با استفاده از matplotlib
در این مرحله میخواهیم دادههای موجود در nparr را با استفاده از نموداری مناسب به تصویر درآوریم. این ماتریس حاوی ۷۸۶۴۳۲ ماتریس یک بعدی است که مقادیر RGB را نگه میدارند. میتوان به این ماتریسهای یک بعدی، به مثابهی مختصات در صفحه R3 نگاه کرد. به این ترتیب با استفاده از تابعی که در زیر به نام show_data نوشتهایم، میتوان دادهها را در صفحه مختصات سه بعدی رسم نمود.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from matplotlib import pyplot as plt def show_data(nparr): fig = plt.figure() ax = fig.add_subplot(111, projection = '3d') ax.scatter(nparr[:, ], nparr[:, 1], nparr[:, 2], c = 'b', marker = '.' ) ax.set_xlabel('Red') ax.set_ylabel('Green') ax.set_zlabel('Blue') plt.show() show_data(nparr) |
برای ترسیم دادهها از کتابخانهی matplotlib پایتون استفاده میکنیم. این کتابخانه، غنیترین کتابخانه پایتون برای رسم انواع نمودارهاست.
با استفاده از متد plt.figure، یک چارچوب و صفحه مختصات ایجاد کردهایم و در ادامه با استفاده از متد fig.add_subplot، به برنامه میگوییم که نمودار ما را به شکل سه بعدی و در کل صفحه رسم کند. متد ax.scatter نیز مسئول دریافت دادههای مد نظر ماست. در این جا پارامتر c نشاندهندهی رنگ نمودار و پارامتر marker نشاندهندهی علامت هر نقطه در نمودار است. در نهایت نیز پس از نامگذاری محورها، نمودار را رسم میکنیم. با فراخوانی تابع برای ماتریس nparr، نمودار زیر به دست میآید:
این نمودار نشاندهندهی آن است که رنگ اکثر پیکسلها در تصویر، به قرمز نزدیک است و قسمتی از پیکسلها نیز آبینگ هستند. این امر، کاملا مطابق انتظار ماست. (با تصویر موجود همخوانی دارد)
یافتن میانگین دادهها | اولین گام از انجام PCA
در این مرحله، وقت آن فرارسیده که گام به گام به پیادهسازی روش PCA بپردازیم. برای انجام آنالیز PCA گامهای اصلی زیر را انجام میدهیم:
- میانگین گرفتن از همه دادهها
- به دست آوردن mean deviation دادهها
- پیدا کردن ماتریس کوواریانس
- یافتن مقادیر ویژه و بردارهای ویژه ماتریس کوواریانس
- ساخت ماتریس w برای انجام تبدیل خطی از فضای R3 به فضای R2
- اعمال ماتریس w بر روی دادهها و به دست آوردن دادههای جدید در فضای R2
- خروجی گرفتن از تصویر جدید به دست آمده
در گام اول و با استفاده از تابع زیر، میانگین دادهها را به دست میآوریم:
1 2 3 4 5 |
#it receives a 2D ndarray and shows the mean of all 1D arrays in it def data_mean(nparr): result = nparr.sum(axis = ) columns = nparr.shape[] return result/columns |
عملکرد این تابع کاملا روشن است. در ابتدا تعداد دادهها را که همان تعداد ستونهای ماتریس nparr هست، در متغیر columns نگه میداریم و پس از آن، جمع همه ستونهای ماتریس را بر تعداد دادهها تقسیم میکنیم تا میانگین به دست آید. میانگین دادهها را چاپ میکنیم و به ماتریس زیر میرسیم:
$ \small \begin{bmatrix} 101.27734595 & 99.45147271 & 119.58644272 \end{bmatrix} $
همان طور که از نمودار رسم شده در قسمت قبل هم روشن بود، شدت رنگ قرمز از دو رنگ دیگر بالاتر است.
ساخت ماتریس کوواریانس و کد پایتون آن
در این مرحله ابتدا باید دادهها به مرکز مختصات ببریم تا کار ما در مراحل بعدی راحتتر باشد. برای فهم این موضوع به دو تصویر زیر توجه میکنیم:
برای بردن دادهها از محلی دلخواه در صفحه مختصات به مرکز مختصات (مطابق آن چه در تصویر بالا روشن است)، کافی است میانگین دادهها را از تک تک داده ها کم کنیم. ماتریس حاصل شده پس از انجام این عملیات را mean deviation مینامیم.
1 2 3 4 5 6 |
def mean_deviation_func(nparr, mean): mean_deviation = np.array(nparr - mean) return mean_deviation nparr_mean = data_mean(nparr) nparr_mean_deviation = mean_deviation_func(nparr, nparr_mean) |
حال میتوان به سراغ یافتن ماتریس کوواریانس رفت. ابتدا توضیحاتی در مورد این ماتریس ارائه میکنیم و سپس روش به دست آوردن آن را بیان مینماییم.
اگر دادههای ما n بعدی باشند، ماتریس کوواریانس، یک ماتریس n*n با ویژگیهای زیر است:
-عناصر موجود بر روی قطر اصلی، نشاندهندهی واریانس دادههای هر بعد هستند.
-عناصر غیر قطر اصلی، نشاندهندهی کوواریانس دو به دوی دادههای بعدها نسبت به هم هستند.
-ماتریس کوواریانس یک ماتریس مربعی متقارن است؛ بنابراین بردارهای ویژه آن، دو به دو بر هم عمودند.
اگر ماتریس mean deviation را B بنامیم، ماتریس کوواریانس را میتوان با فرمول زیر به دست آورد:
$ S = \frac{1}{N-1}BB^{T} $
1 2 3 4 5 6 |
def covariance_matrix(nparr_mean_deviation): transpose = nparr_mean_deviation.transpose() columns = nparr_mean_deviation.shape[] return (1 / (columns - 1)) * np.dot(transpose, nparr_mean_deviation) covariance_matrix = covariance_matrix(nparr_mean_deviation) |
VIII. یافتن مقدار واریانس و کوواریانس دادهها | کاربرد PCA در پردازش تصویر
با استفاده از کد قسمت قبل، ماتریس کوواریانس به شکل زیر به دست آمد (قسمت اعشاری حذف شده است):
$ \small \begin{bmatrix} \begin{bmatrix} 3906 & 3705 &3461 \end{bmatrix}, \\ \begin{bmatrix} 3705 & 6590 &8208 \end{bmatrix}, \\ \begin{bmatrix} 3461 & 8208 & 11124 \end{bmatrix} \end{bmatrix} $
1 2 3 4 5 6 7 8 9 10 11 12 13 |
print("variance of Red is:") print(covariance_matrix[, ]) print("variance of Green is:") print(covariance_matrix[1, 1]) print("variance of Blue is:") print(covariance_matrix[2, 2]) print("covariance between Red and Green is:") print(covariance_matrix[, 1]) print("covariance between Red and Blue is:") print(covariance_matrix[, 2]) print("covariance between Blue and Green is:") print(covariance_matrix[1, 2]) |
کاهش بعد با روش PCA
تا این جای کار ماتریس کوواریانس را ساختیم. حال باید یک تبدیل خطی بیابیم که فضای R3 ما را به فضای R2 تبدیل کند و کاهش بعد داشته باشیم. برای ساخت ماتریس متناظر با این تبدیل خطی (ماتریس w) نیازمندیم که مقادیر ویژه و بردارهای ویژه (eigenpairs) ماتریس کوواریانس را پیدا کنیم. اگر برخی از مقادیر ویژه به شکل قابل توجهی از دیگر مقادیر بزرگتر باشند میتوان کاهش بعد را انجام داد. به این صورت که بردارهای ویژه متناظر با مقادیر ویژه کوچک کنار گذاشته میشود و با بردارهای ویژه باقیمانده، یک ماتریس (همان ماتریس w) ساخته میشود. این ماتریس، تبدیل خطی مورد نیاز را در اختیار ما قرار میدهد.
تابع زیر، مقادیر ویژه و بردارهای ویژه را در اختیار ما قرار میدهد:
1 2 3 4 5 6 7 8 9 10 |
#returns a tuple of (eigenvalues, eigenvectors) def eigen(covariance_matrix): return np.linalg.eig(covariance_matrix) eigenvalues, eigenvectors = eigen(covariance_matrix) print("eigenvalues are:") print(eigenvalues) print("eigenvectors are:") print(eigenvectors) |
با اجرای کد بالا، مقادیر ویژه و بردارهای ویژه به صورت زیر به دست میآید (اعشار حذف شده است):
$ \small \begin{bmatrix} 19030 & 2549 & 41 \end{bmatrix} $
$ \small \begin{bmatrix} \begin{bmatrix} -0.31 & -0.89 &0.33 \end{bmatrix}, \\ \begin{bmatrix} -0.59 & -0.91 &-0.80 \end{bmatrix}, \\ \begin{bmatrix} -0.75 & 0.45 & 0.49 \end{bmatrix} \end{bmatrix} $
همان طور که مشاهده میشود، بین اندازه مقادیر ویژه، اختلاف فاحشی است. برای روشنتر شدن این اختلاف، با استفاده از کد زیر، نمودار میلهای درصد همه مقادیر ویژه را از جمع کل آنها به دست میآوریم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def explained_variance(eigenvalues): total = sum(eigenvalues) exp_variance = [(i / total)*100 for i in eigenvalues] objects = ('Red', 'Green', 'Blue') y_pos = np.arange(len(objects)) plt.bar(y_pos, exp_variance, align='center', alpha=0.5) plt.xticks(y_pos, objects) plt.ylabel('Percentage') plt.title('Explained Variance') plt.show() #explained_variance(eigenvalues) explained_variance(eigenvalues) |
نمودار به دست آمده به شکل زیر است:
به وضوح مشخص است که با حذف دادههای بعد آبی، دادههای زیادی از دست نمیروند و میتوان این کاهش بعد را انجام داد.
حال کافی است ابتدا مقادیر ویژه و بردارهای ویژه را از بزرگ به کوچک مرتب کرده و دو بردار ویژه اول را به عنوان ستونهای ماتریس w بیرون بکشیم و این ماتریس را بسازیم. در کد زیر، این فرایند را انجام دادهایم:
1 2 3 4 5 6 7 |
#sorting the eigen pairs in descending order eig_pairs = [(np.abs(eigenvalues[i]), eigenvectors[:,i]) for i in range(len(eigenvalues))] eig_pairs.sort(key=lambda x: x[], reverse=True) #constructing matrix w matrix_w = np.hstack((eig_pairs[][1].reshape(3,1), eig_pairs[1][1].reshape(3,1))) print('Matrix W:\n', matrix_w) |
تنها یک مرحله دیگر باقی مانده است! کافی است ماتریس w را بر روی دیتاست اعمال کنیم تا دیتای سه بعدی ما به یک دیتای دو بعدی تبدیل شود. توجه داریم که w یک ماتریس ۲*۳ است و ماتریس با سه بعدی را به ماتریس دو بعدی تبدیل میکند. با استفاده از قطعه کد زیر میتوان w را از راست در دیتای قبلی ضرب کرد تا دیتاست جدید با نام new_arr به دست آید. در نهایت این دیتاست دو بعدی را در صفحه مختصات R2 رسم میکنیم:
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 |
def show_data_2d(nparr): # x-axis values x = nparr[:, ] # y-axis values y = nparr[:, 1] # plotting points as a scatter plot plt.scatter(x, y, label= "pixels", color= "green", marker= ".", s=30) # x-axis label plt.xlabel('principle component 1') # frequency label plt.ylabel('principle component 2') # plot title plt.title('New Space') # showing legend plt.legend() # function to show the plot plt.show() #mapping the dataset to new space and printing it new_arr = nparr.dot(matrix_w) #show_data_2d(new_arr) show_data_2d(new_arr) |
نمودار به دست آمده به شکل زیر است:
حال همه چیز فراهم است که عکس جدیدی بسازیم و از آن خروجی بگیریم. ابتدا به جای بعد سومی که کاهش یافته است، صفر میگذاریم و با استفاده از کتابخانه pillow و متد save، تصویر جدید را ذخیره میکنیم.
توجه: از astype برای تبدیل datatype به unit8 استفاده میکنیم؛ زیرا کتابخانه PIL چنین دادهای را میپذیرد.
1 2 3 4 5 6 |
#constructing new image array new_arr_with_zeros = np.hstack([new_arr, np.zeros([height * width, 1])]) new_image = Image.fromarray(np.reshape(new_arr_with_zeros, (height, width, 3)).astype('uint8')) #saving the new image new_image.save("new_deset.jpg") |
در انتها نیز تصویر جدید را با تصویر اولیه مقایسه میکنیم:
جمع بندی
در این پروژه با یکی از تکنیکهای کاهش بعد با نام PCA آشنا شدیم و میتوان با استفاده از آن، بعد دیتاستهای مختلف را کاهش داد. ما در این پروژه با هدفهای آموزشی، به طور مفصل کدنویسی کردیم؛ به جای همهی این مراحل میتوان با استفاده از کتابخانه scikit learn در پایتون، آنالیز PCA را با استفاده از کد زیر انجام داد:
1 2 3 |
from sklearn.decomposition import PCA as sklearnPCA sklearn_pca = sklearnPCA(n_components=2) new_arr = sklearn_pca.fit_transform(nparr) |
در استفاده از بسیاری از الگوریتمهای یادگیری ماشین که با فیچرهای زیادی سر و کار داریم، بهتر است ابتدا با استفاده از تکنیکهای کاهش بعد مانند PCA، فیچرهای مربوط به هم را حذف کرد تا عملکرد الگوریتم یادگیری ماشین بهینهتر شود.
منابع:
- Linear Algebra and its Applications by David C. Lay – section 7.5
- https://en.wikipedia.org/wiki/RGB_color_model
- https://sebastianraschka.com/
دم شما گرم. عالی بود
ممنون از توجه شما
دم شما گرم