فرض کنیم مجموعه بزرگی از متون در اختیار داریم و می خواهیم بدانیم این متون درباره چه موضوعاتی هستند؟ در این حالت الگوریتم های مدلسازی موضوعی ( topic modeling) به کارمان می آیند. مدلسازی موضوعی به دنبال این است که مجموعه از موضوعات را که اسناد یک مجموعه درباره آنها هستند را پیدا کند. ما در این پست از الگوریتم LDA که مخفف عبارت Latent Dirichlet Allocation است برای مدلسازی موضوعی استفاده خواهیم کرد. اگر می خواهید در رابطه با مدلسازی موضوعی بیشتر بدانید این پست را بخوانید که به زبان فارسی است و روشهای مختلف مدلسازی موضوعی را بررسی کرده است و نقطه شروع خوبی برای شناختن عمیق تر این روشهاست.
در این پست دنبال رسیدن به این اهداف هستیم:
1- پیش زمینه ای از مدلسازی موضوعی خواهیم گفت
2- از الگوریتم LDA استفاده خواهیم کرد. این الگوریتم ریاضیات پیچیده ای دارد که ما واردش نمی شویم اما به صورت خیلی شهودی توضیح میدهیم که این الگوریتم چطور کار می کند.
3- از کتابخانه topicmodelig در R استفاده خواهیم کرد.
4- قدم به قدم جزییات را شرح میدهیم و به منابع دیگری لینک میدهیم، بخصوص درباره مباحثی که جای شرح بیشتر آنها را در این وبلاگ نداریم.
LDA چطور کار می کند؟
در واقع اگر بخواهیم دقیق بدانیم که LDA چطور موضوعات را در یک مجموعه متن شناسایی می کند باید مبنای ریاضیاتی و آماری آن را بررسی کنیم. اما با کنار گذاشتن ریاضیات این مدل هم می توان درکی شهودی از آن بدست آورد.
فرض کنیم مجموعه بزرگی از متن در اختیار داریم. ( بزرگ بودن مجموعه و همین طور کوتاه نبودن متن ها در این روش اهمیت دارد). LDA فرض می کند در این مجموعه متن تعدادی، مثلا 10، موضوع وجود داشته باشد. فرض بعدی این است که هر یک از این موضوعات می تواند سهمی در تشکیل هر یک اسناد موجود در مجموعه متن داشته باشد. یعنی اگر فرض بر این باشد که در مجموع 10 موضوع مختلف داریم. هر یک از اسناد ترکیبی است از تمام این موضوعات ، البته نه با احتمال مساوی! بلکه ممکن است یک یا چند موضوع سهم بیشتری در یک سند داشته باشند.
ولی ما در واقعیت فقط متون و کلمات را می بینیم و نه موضوعات را! موضوعات در واقع در متن ها پنهان هستند و هدف LDA استخراج این موضوعات پنهان با داشتن متن ها و کلمات است. خوب حالا LDA چطور این موضوعات پنهان را پیدا می کند؟
بر اساس جواب به یک سوال در سایت Quora که توسط ادوین چن نوشته شده است، LDA به زبان ساده به این ترتیب کار می کند :
بعد از تکرار مراحل بالا به تعداد زیاد به یک وضعیت نسبتا ثابت خواهیم رسید که در آن موضوع هایی که به هر کلمه نسبت داد هایم دیگر تغییر نمی کنند و مدل بدست آمده مدل موضوعی مجموعه متن خواهد بود.
مدلسازی موضوعی با استفاده از پکیج topicmodels در R
در ادامه این پست، با استفاده از پکیج topicmodels در R، روی یک مجموعه کوچک (بله کوچک، برخلاف آنکه گفتیم مجموعه اسناد باید بزرگ باشد! ) الگوریتم LDA را اجرا می کنیم و مدلسازی موضوعی را انجام می دهیم. الگوریتم LDA در این پکیج تعداد زیادی ورودی دارد که ما اینجا فقط آنهایی را که استفاده کرده ایم توضیح می دهیم. برای بررسی بیشتر به مستندات پکیج topicmodeling مراجعه کنید.
مجموعه متن مورد استفاده ما در این پست که می توانید آن را از اینجا دریافت کنید شامل 30 متن خبری است که از صفحه اخبار سیاسی خبرگزاری های ایرنا، ایسنا و فارس انتخاب شده اند. برای این کار در روز یکشنبه 8 اردیبهشت 1398، 10 خبر اول ( که در جایگاه های اخبار مهم و نیز سایر اخبار قرار داشتند ) از هریک از این خبرگزاری ها انتخاب شد.
قدم اول بارگزاری کتابخانه "tm" است. ( اگر این کتابخانه را قبلا نصب نکرده اید لازم است قبل از فراخوانی این دستورها، با دستور install.package(“tm”) آنها را نصب کنید) :
##### load 'tm' package ##### library(tm) |
در مرحله بعد فایلهای اخبار را که در یک فولدر به نام data قرار دارند خوانده و در یک dataframe قرار می دهیم.
##### read news files and convert them into a dataframe ##### newsFiles_dir <-"./data" newsFiles <- list.files(newsFiles_dir , pattern = "*.txt" , full.names = TRUE) txt_data<- lapply(newsFiles , function( x ) { strs <- read.delim (x , stringsAsFactors = FALSE) paste(strs , collapse = ' ') } ) data<- data.frame( doc_id = newsFiles , text = as.character( unlist( do.call(rbind , txt_data)))) |
در مرحله بعدی، مراحل پاکسازی متن را انجام می دهیم. در این مرحله علائم نگارشی و کاراکترهای کنترلی و .. را از متن حذف می کنیم :
##### remove punctuations and control characters ##### data[[2]] <- gsub(" ها" , " " , data[[2]]) data[[2]] <- gsub("'" , " " , data[[2]]) data[[2]] <- gsub("[[:punct:]]" , " " , data[[2]]) data[[2]] <- gsub("[[:cntrl:]]" , " " , data[[2]]) data[[2]] <- gsub("^[[:space:]]+" , "" , data[[2]]) data[[2]] <- gsub("[[:space:]]+$" , "" , data[[2]]) |
پس از آن corpus را ایجاد می کنیم و سپس کلمات توقف را حذف می کنیم. corpus یک شی است که نماینده یک مجموعه متن است. ما با خواندن و تبدیل مجموعه متن به corpus می توانیم از توابعی که پکیج tm برای پردازش متن ارائه کرده استفاده کنیم و در نهایت آن را تبدیل به ماتریس ترم-داکیومنت کنیم. لیستی از کلمات توقف که برای این کار آماده و از آن استفاده شده است را از اینجا می توانید دریافت کنید.
##### create corpus object ##### docs <- Corpus (VectorSource(data[[2]]) , readerControl = list(language="fa_FAE",encoding = "UTF-8"))
persianStopwords_file_loc = "./files/stopwords.txt" persianStopwords<- readLines(persianStopwords_file_loc ) docs <- tm_map(docs , removeWords , persianStopwords) |
ماتریس ترم-داکیومنت در واقع یک ماتریس است که به تعداد متن های مجموعه ما سطر و به تعداد کلمات آن ستون دارد. نوع بازنمایی عددی مجموعه متن که آن را آماده می کند تا به عنوان ورودی به LDA داده شود. دستورات زیر corpus بدست آمده در مرحله قبل را به ماتریس ترم- داکیومنت تبدیل می کنند.
##### create doctment-term matrix ##### dm <- DocumentTermMatrix(docs,control = list (encoding = 'UTF-8')) rownames(dm) <- data[[1]] ##### remove empty documents , if any ##### rowTotals <- apply(dm , 1 , sum) dtm <- dm [rowTotals>0 , ] |
همانطور که گفته شد در واقع تمام تلاش های ما تا این مرحله برای به دست آوردن ماتریس ترم- داکیومنت بود چرا که این ورودی ای است که به تابع LDA خواهیم داد. کاری که تا اینجا انجام دادیم تقریبا پیش نیاز تمام الگوریتم های پردازش متن است. اگر می خواهید درباره پردازش متن درR بیشتر بدانید اینجا را بخوانید.
علاوه بر این ماتریس ترم داکیومنت پارامتر دیگری که به تابع LDA باید بدهید k، تعداد موضوع ها و method است. پارامتر method روشی است که LDA برای بهینه سازی مدل از آن استفاده می کند. طبق مستندات پکیج topicmodels پارامتر method می تواند دو مقدار متفاوت داشته باشد "VEM" و "Gibbs" که مقدار پیش فرض آن "VEM". این پست وبلاگ eight2late که من از آن استفاده زیادی در این پست کرده ام از روش "Gibbs" استفاده کرده است. و من هم به تبعیت از آن از این روش استفاده می کنم. سایر پارامترهایی که اینجا به LDA داده ایم در واقع پارامترهای مورد نیاز روش "Gibbs" هستند که در همان پست شرح داده شده اند و من از آوردن آنها در اینجا خودداری می کنم.
باید به این نکته توجه داشت که مشخصات و پارامترهای بالا تضمین نمی کند که راه حل حاصل، راه حل بهینه سراسری باشد و در واقع متد Gibbs در بهترین حالت یک راه حل بهینه محلی را پیدا می کند. بهترین راه این است که این الگوریتم را با پارامترهای متفاوت اجرا کنید و بهترین نتیجه را استفاده کنید. البته این کار با حجم داده زیاد غیرعملی است بنابراین از آنجایی که ما دنبال نتایج عملی هستیم در صورتی که نتایج برایمان راضی کننده باشد به همان صورت آن را می پذیریم!
##### load topicmodels library ##### library(topicmodels) #model paramaters burnin = 1000 iter = 1000 keep = 50 k = 10 ##### train topic models ##### model <- LDA(dtm , k=k , method = "Gibbs" , control = list(burnin = burnin , iter = iter , keep = keep)) |
خروجی تابع LDA یک شی است با کلی اطلاعات از نتیجه اجرای این الگوریتم. یکی از این نتایج موضوع اختصاص داده شده به هر یک از متن هاست. در قطعه کد زیر، ما این موضوع ها با فراخوانی تابع topics به دست آورده و بعد نتایج را نمایش داده ایم.
##### topics assigned to each document ##### model.topics <- as.matrix(topics(model)) model.topics |
نتیجه به دست آمده برای این بخش به صورت زیر است :
topic id | doc name |
1 | ./data/fars10.txt |
7 | ./data/fars1.txt |
2 | ./data/fars2.txt |
6 | ./data/fars3.txt |
9 | ./data/fars4.txt |
3 | ./data/fars5.txt |
7 | ./data/fars6.txt |
8 | ./data/fars7.txt |
8 | ./data/fars8.txt |
8 | ./data/fars9.txt |
4 | ./data/irna10.txt |
2 | ./data/irna1.txt |
4 | ./data/irna2.txt |
5 | ./data/irna3.txt |
10 | ./data/irna4.txt |
10 | ./data/irna5.txt |
3 | ./data/irna6.txt |
1 | ./data/irna7.txt |
9 | ./data/irna8.txt |
2 | ./data/irna9.txt |
5 | ./data/isna3.txt |
9 | ./data/isna8.txt |
##### how many documents in each topic ##### table(model.topics) |
که نتیجه آن :
علاوه بر موضوع اختصاص داده شده به هر متن می توانیم کلمات هر موضوع را بر حسب میزان اهمیت آن کلمه در موضوع ببینیم:
##### print top 5 important words in each topic ##### print(terms(model , 5)) |
که نتیجه آن یه این صورت است :
و در نهایت ابر کلمات، متن های دو موضوع متفاوت را با هم مقایسه کنیم. برای اینکار ابتدا تابع plotWordCloud را نوشته ایم که با گرفتن اندیس هر موضوع ابر کلمات آن را رسم می کند و پس از آن این تابع را برای 2 موضوع متفاوت فراخوانی کرده ایم که نتیجه را مشاهده می کنید.
##### function to plot wordcloud for a given topic ##### plotWordCloud<- function(t) { m<- as.matrix(dtm[rownames(model.topics)[which(model.topics==t)], ]) require(wordcloud) v<- sort(colSums(m) , decreasing = TRUE) words<- names(v) d<- data.frame(words= words , frq=v) dark2<- brewer.pal(6,"Dark2") wordcloud(d$words , d$frq , min.freq = 2 , colors = dark2) } ##### plot wordcloud of two topics and compare them! ##### par(mfrow=c(1,2)) plotWordCloud(1) plotWordCloud(8) |