کاهش بُعد و مصورسازی داده ها به روش t-SNE

مقدمه

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

اخیراً، فرصتی پیش آمد که درباره روش جاگذاری همسایگی تصادفی با توزیع احتمالی t (یا t-Distributed Stochastic Neighbor Embedding) که به روش t-SNE نیز مشهور است، مطالعه ای داشته باشم. در این مطلب تلاش خواهم کرد مروری کلی بر روی این الگوریتم داشته باشم. تعدادی کُد نمونه پایتون (Python) نیز با شما به اشتراک خواهم گذاشت که برای آزمایش روش t-SNE بر روی مجموعه داده های Digits و همین طور مجموعه داده MNIST نوشته شده اند.

t-SNE چیست؟

الگوریتم جاگذاری همسایگی تصادفی با توزیع احتمالی t یکی از تکنیک های غیرخطی نظارت نشده (Unsupervised) معرفی شده به عرصه علوم داده و یادگیری ماشین است که اساساً برای مصورسازی داده های با بُعد بالا (فراتر از ۳) طراحی شده است. به زبانی دیگر، t-SNE تصویری ساده سازی شده از چیدمان و ساختار داده ها در فضاهای با ابعاد بالا (High-dimensional)‌ در چهارچوب های محدود دو یا سه بعدی ارائه می دهد. این الگوریتم در سال ۲۰۰۸ توسط لورن وان دِر ماتن (Laurens van der Maatens) و جفری هینتون (Geoffrey Hinton) توسعه یافته است.

t-SNE در مقایسه با PCA

اگر با روش تحلیل مولفه های اصلی (Principle Components Analysis یا PCA) آشنایی داشته باشید، طبعاً باید مثل من کنجکاو باشید که تفاوت t-SNE و PCA را بدانید. اولین نکته آن است که PCA در سال ۱۹۳۳ میلادی معرفی شده ولی t-SNE چندی پیش و در سال ۲۰۰۸ میلادی! دنیای علوم داده از سال ۱۹۳۳ تاکنون تغییرات فراوانی داشته است و مهم ترین آن ها تغییر در توان پردازشی در دسترس (به واسطه تولید کامپیوترها، پردازنده های قدرتمند و اخیراً فناوری محاسبات ابری) و همچنین تغییر شگفت انگیز اندازه مجموعه داده هاست. از سوی دیگر، PCA یک تکنیک خطی (Linear) کاهش ابعاد داده است که تلاش می کند پراکنش (Variance) داده را بیشینه نموده و هم زمان زوج های با فواصل بزرگ را حفظ کند. به بیانی دیگر، عناصری از داده که تفاوت معنی داری دارند در فاصله دوری از هم قرار خواهند گرفت. این شیوه می تواند به مصورسازی ضعیف و غیرقابل قبولی منتهی شود خصوصاً وقتی با ساختارهای متنوع غیرخطی (کروی، منحنی، سیلندری و …) سر و کار داریم.

تفاوت t-SNE با PCA در آن است که بر روی حفظ فاصله های کوچک بین زوج ها و مشابهت های محلی تأکید دارد در حالی که PCA بر حفظ فواصل بزرگ بین زوج ها برای بیشینه کردن پراکنش متمرکز است. لورن، یکی از توسعه دهندگان الگوریتم، تفاوت t-SNE و PCA را به خوبی به کمک مجموعه داده Swiss Roll (تصویر ۱)‌ نمایش می دهد. می توانید ببینید که با توجه به طبیعت غیرخطی این مجموعه داده و در صورت حفظ فواصل بزرگ (روش PCA) ساختار داده به اشتباه تشخیص داده می شود.

تصویر ۱: مجموعه داده Swiss Roll. خط ممتد خروجی اعمال t-SNE و حفظ فواصل کوچک و خط چین نتیجه اعمال PCA با هدف بیشینه کردن پراکنش است.

t-SNE چگونه کار می کند؟

حال که می دانید چرا باید به جای PCA به سراغ t-SNE برویم، بیایید ببینیم که t-SNE چگونه کار می کند. این الگوریتم نوعی معیار مشابهت (Similarity Measure) بین زوج-داده ها در فضای دارای بُعد بالا و فضای با بُعد پایین محاسبه می کند. سپس تلاش می کند که این دو معیار مشابهت را از طریق یک تابع هزینه (Cost Function) بهینه کند. اجازه دهید الگوریتم را به سه گام خُرد کرده و به تشریح آن ها بپردازیم.

۱. گام اول، محاسبه مشابهت ها بین نقاط داده در فضای با بُعد بالا. تعدادی نقاط داده (Data Point)‌ را که در یک فضای دو بعدی پخش شده اند، تصور کنید (تصویر ۲). برای هر نقطه x_i یک توزیع احتمالی گاوسی (Gaussian Distribution) به مرکزیت آن نقطه تخصیص داده می شود. سپس چگالی احتمالی (Probability Density)‌ سایر نقاط (x_j) تحت آن توزیع گاوسی محاسبه می شود. بعد از نرمالیزه کردن نتایج حاصل، مجموعه از احتمالات P_{ij} برای همه نقاط خواهیم داشت. این احتمال ها متناظر با مشابهت ها هستند. به این معنی که، اگر نقاط x_1 و x_2 مقادیر یکسانی تحت این دایره گاوسی داشته باشند، مشابهت آن ها یکسان بوده و در نتیجه در این ساختار مشابهت محلی در فضای با بُعد بالا وجود دارد. توزیع یا دایره گاوسی به کمک مفهومی به نام پیچیدگی (Perplexity)‌ قابل دستکاری است. در واقع پراکنش توزیع (به نوعی همان قطر دایره) و تعداد نزدیک ترین همسایگان قابل تنظیم هستند. مقدار نرمال برای پارامتر پیچیدگی بین ۵ تا ۵۰ است.

تصویر ۲:‌ اندازه گیری مشابهت زوج نقاط در فضای با بُعد بالا

۲. گام دوم مشابه گام نخست است با این تفاوت که به جای استفاده از یک توزیع احتمالی گاوسی از توزیع t (یا Student t-distribution) با یک درجه آزادی (degree of freedom) استفاده می شود. این توزیع تحت عنوان توزیع کوشی (Cauchy Distribution) نیز شناخته می شود (تصویر ۳). نتیجه این کار، مجموعه ثانویه ای از احتمالات (Q_{ij}) در فضای با بُعد پایین است. چنان چه مشاهده می کنید توزیع t در دو سو، دنباله های کشیده تری در مقایسه با توزیع نرمال (Guassian) دارد. این ویژگی به مُدل کردن زوج نقاط با فواصل زیاد کمک شایانی می کند.

تصویر ۳: توزیع احتمالی نرمال در مقایسه با توزیع t

۳. در گام نهایی می خواهیم مجموعه احتمالات محاسبه شده از فضای با بُعد پایین (Q_{ij}) به بهترین شکل بازتاب دهنده احتمالات P_{ij} از فضای با بُعد بالاتر باشند. در حقیقت، نقشه دو ساختار احتمالاتی استخراج شده بایستی مشابه هم باشند. ما تفاوت بین توزیع های احتمالی حاصل از دو فضا را به کمک دیورژانس کالبک-لیبلر (Kullback-Liebler یا KL) اندازه می گیریم. نمی خواهیم وارد جزئیات KL بشویم. به طور خلاصه، KL رویکردی غیرمتقارن است که با کارآیی بالا مقادیر P_{ij} و Q_{ij} را مقایسه می کند. در نهایت، به کمک روش گرادیان نزولی (Gradient Descent) تابع هزینه دیورژانس KL را کمینه می کنیم.

موارد استفاده از t-SNE

حال که می دانید t-SNE چگونه کار می کند، اجازه دهید مروری سریع به جایگاه هایی که می توان از آن استفاده کرد اشاره کنیم. لورن وان دِر ماتن نمونه های بسیاری را در ارائه ویدئویی خود به نمایش گذاشت. وی به امکان استفاده از t-SNE در تحقیقات آب و هوایی، امنیت کامپیوتری، زیست-انفورماتیک (Bioinformatics)، تحقیقات سرطان و … اشاره کرد. t-SNE بر روی مجموعه داده های با بُعد بالا قابل اعمال است و سپس خروجی الگوریتم را می توان به عنوان ورودی مدل های طبقه بندی (Classification) استفاده نمود.

علاوه بر آن، می توان از t-SNE برای بررسی، آموزش، و یا ارزیابی بخش بندی داده ها (Segmentation)‌ استفاده نمود. بسیاری اوقات ما تعداد بخش ها (خوشه ها یا Clusters) را پیش از مدل سازی انتخاب می کنیم. اما با به کار بردن t-SNE، در بسیاری اوقات می توانیم بدون از پیش تعیین کردن تعداد بخش ها، جدایش (Separation) موجود بین نقاط داده را مشاهده کنیم. البته t-SNE یک رویکرد خوشه بندی نیست چرا که ورودی را به سان PCA حفظ نمی کند و ممکن است مقادیر هر نقطه داده در طی اجرای الگوریتم تغییر یابند. اساساً t-SNE برای مصورسازی و کنکاش بصری در داده ها تعریف شده است.

نمونه کُد

در زیر نمونه هایی از کُدهای پایتون آورده شده است که مقایسه تصویری ای بین PCA و t-SNE پس از اعمال هر دو بر روی مجموعه داده های Digits و MNIST را به نمایش گذاشته اند. ما هر دوی این مجموعه داده ها را انتخاب کردیم،‌ چرا که به لحاظ ابعادی متفاوت هستند و نتایج متفاوتی نیز پس از اعمال الگوریتم ها بر روی آن ها حاصل می شود. درون کُد تکنیکی استفاده شده است که می توانید PCA را پیش از اعمال t-SNE اجرا کنید. در واقع این کار برای کاهش بار محاسباتی انجام شده است. شما عموماً ابتدا به کمک PCA ابعاد داده را به حدود ۳۰ بُعد کاهش می دهید و سپس t-SNE را اجرا می کنید.

من کُدهای زیر را با استفاده از پایتون و با فراخوانی کتابخانه های SAS اجرا کردم. ساختار کُدها ممکن است کمی با آن چه شما به آن عادت دارید متفاوت باشد. برای مصورسازی از کتابخانه Seaborn استفاده شده است. البته با t-SNE ممکن است خوشه های کاملاً فشرده ای حاصل شود که برای مشاهده بهتر نتایج شاید مجبور باشید زوم کنید. اگر به آزادی عمل بیشتری برای کند و کاو تصویری در داده ها و نتایج الگوریتم ها نیاز دارید، شاید کتابخانه Plotly گزینه بهتری باشد. اگر از Matplotlib برای مصورسازی استفاده می کنید، افزودن کُد %matplotlib notebook در ابتدای قطعه کُد امکانات مشابهی را در اختیار شما قرار می دهد.

گام ۱ – فراخوانی کتابخانه های پایتون، ایجاد ارتباط با سرور SAS (که در این مورد CAS نام دارد و یک موتور توزیع شده در حافظه است)، فراخوانی مجموعه عملگرهای CAS (چیزی شبیه کتابخانه های برنامه نویسی)، خواندن داده ها و مشاهده ساختار داده.

# Load Python Libraries
import swat
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from time import time
%matplotlib inline

# Create CAS Connection
conn = swat.CAS(host, portnum, protocol='http')
conn.sessionProp.setSessOpt(messageLevel='NONE'); # Suppress CAS Messages

# Load CAS Action Sets
conn.loadactionset('pca')
conn.loadactionset('tsne');

# Read in Data
digits = pd.read_csv('/Users/anviol/Desktop/Content/Datasets/Digits/digits.csv')
mnist = pd.read_csv('/Users/anviol/Desktop/Content/Datasets/mnist/small_mnist.csv')
display(digits.shape)
display(mnist.shape)

گام ۲ – تا این جا، کماکان در حال کار کردن روی کامپیوتر شخصی ام هستم. حال داده ها را درون سرور CAS که پیش تر معرفی کردیم، فراخوانی می کنیم. با این کار، از توان پردازشی فوق العاده این سرور بهره مند خواهیم شد. سپس الگوریتم PCA را بر روی هر دو مجموعه داده Digits و MNIST اعمال می کنیم.

# Upload Digits and MNIST Data to CAS Server
digits_cas = conn.upload_frame(digits, casout=dict(name='digits_df', replace=True))
mnist_cas = conn.upload_frame(mnist, casout=dict(name='mnist_df', replace=True));

# Perform PCA: Digits
pca_digits = conn.eig(table='digits_df',
                      n=30,
                      inputs=list(digits.drop(['ID', 'Label'], axis=1)),
                      output={'casOut':{'name':'pca_digits','replace':'TRUE'},
                              'copyVars':['ID','Label']})

# Perform PCA: MNIST
pca_mnist = conn.eig(table='mnist_df',
                     n=30,
                     output={'casOut':{'name':'pca_mnist','replace':'TRUE'},
                             'copyVars':['ID','Label']})

گام ۳ – مصور سازی نتایج PCA برای هر دو مجموعه داده Digits و MNIST

# Plot Digits PCA

# Set style of scatterplot
sns.set_context("notebook", font_scale=1.1)
sns.set_style("ticks")

# Create scatterplot of dataframe
sns.lmplot(x='Score1',
           y='Score2',
           data=pca_digits_out,
           fit_reg=False,
           legend=True,
           size=9,
           hue='Label',
           scatter_kws={"s":200, "alpha":0.3})

plt.title('PCA Results: Digits', weight='bold').set_fontsize('14')
plt.xlabel('Prin Comp 1', weight='bold').set_fontsize('10')
plt.ylabel('Prin Comp 2', weight='bold').set_fontsize('10')
PCA نتایج قابل توجهی بر روی مجموعه داده Digits حاصل کرده و ساختار داده را به خوبی کشف نموده است.
# Plot MNIST PCA
sns.set_context("notebook", font_scale=1.1)
sns.set_style("ticks")

sns.lmplot(x='Score1',
           y='Score2',
           data=pca_mnist_out,
           fit_reg=False,
           legend=True,
           size=9,
           hue='Label',
           scatter_kws={"s":200, "alpha":0.3})

plt.title('PCA Results: MNIST', weight='bold').set_fontsize('14')
plt.xlabel('Prin Comp 1', weight='bold').set_fontsize('10')
plt.ylabel('Prin Comp 2', weight='bold').set_fontsize('10')
چنان چه مشاهده می کنید، PCA بر روی مجموعه داده MNIST مشکل ازدحام (Crowding) دارد (به سمت راست تصویر دقت کنید).

گام ۴ – حال مراحل قبلی را این بار به کمک الگوریتم t-SNE تکرار می کنیم.

# Perform t-SNE: Digits
tsne_digits = conn.tSne(table='digits_df',
                        inputs = list(digits.drop(['ID', 'Label'], axis=1)),
                        nDimensions = 2,
                        maxIters = 1000,
                        perplexity = 100,
                        learningRate = 1000,
                        seed = 24680,
                        output= {'casout':{'name':'tsne_digits','replace':'TRUE'},
                                 'copyVars':['ID','Label']})

# Plot Digits t-SNE
sns.set_context("notebook", font_scale=1.1)
sns.set_style("ticks")

sns.lmplot(x='_DIM_1_',
           y='_DIM_2_',
           data=tsne_digits_out,
           fit_reg=False,
           legend=True,
           size=9,
           hue='Label',
           scatter_kws={"s":200, "alpha":0.3})

plt.title('t-SNE Results: Digits', weight='bold').set_fontsize('14')
plt.xlabel('Dimension 1', weight='bold').set_fontsize('10')
plt.ylabel('Dimension 2', weight='bold').set_fontsize('10')

و اکنون t-SNE بر روی مجموعه داده MNIST …

# Perform t-SNE: MNIST
tsne_MNIST = conn.tSne(table='mnist_df',
                       inputs = list(mnist.drop(['ID', 'Label'], axis=1)),
                       nDimensions = 2,
                       maxIters = 1000,
                       perplexity = 100,
                       learningRate = 1000,
                       seed = 24680,
                       output= {'casout':{'name':'tsne_MNIST','replace':'TRUE'},
                                'copyVars':['ID','Label']})

# Plot MNIST t-SNE
sns.set_context("notebook", font_scale=1.1)
sns.set_style("ticks")

sns.lmplot(x='_DIM_1_',
           y='_DIM_2_',
           data=tsne_MNIST_out,
           fit_reg=False,
           legend=True,
           size=9,
           hue='Label',
           scatter_kws={"s":200, "alpha":0.3})

plt.title('t-SNE Results: MNIST', weight='bold').set_fontsize('14')
plt.xlabel('Dimension 1', weight='bold').set_fontsize('10')
plt.ylabel('Dimension 2', weight='bold').set_fontsize('10')

نتیجه گیری

امیدوارم که از مرور انجام شده و نمونه کُدهای پایتون برای معرفی الگوریتم t-SNE لذت برده باشید. از آن رو که اکثر داده هایی که امروزه با آن ها سر و کار داریم دارای ابعاد بالا هستند، t-SNE ابزاری جذاب و قدرت مند برای مصورسازی این داده ها خواهد بود. پیشنهاد می کنم برای درک بهتر این الگوریتم، سری به ویدئوهای لورن (یکی از نویسندگان مقاله پژوهشی t-SNE) بر روی YouTube بزنید.