دیتا اینسایتز

در مورد علم داده، داده کاوی، هوشمندی کسب و کار(BI) و سایر مطالب مرتبط

دیتا اینسایتز

در مورد علم داده، داده کاوی، هوشمندی کسب و کار(BI) و سایر مطالب مرتبط

مدلسازی موضوعی در R

فرض کنیم مجموعه بزرگی از متون در اختیار داریم و می خواهیم بدانیم این متون درباره چه موضوعاتی هستند؟ در این حالت الگوریتم های مدلسازی موضوعی ( topic modeling) به کارمان می آیند. مدلسازی موضوعی به دنبال این است که مجموعه از موضوعات را که اسناد یک مجموعه درباره آنها هستند را پیدا کند.  ما در این پست از الگوریتم LDA که مخفف عبارت Latent Dirichlet Allocation است برای مدلسازی موضوعی استفاده خواهیم کرد. اگر می خواهید در رابطه با مدلسازی موضوعی بیشتر بدانید این پست را بخوانید که به زبان فارسی است و روشهای مختلف مدلسازی موضوعی را بررسی کرده است و نقطه شروع خوبی برای شناختن عمیق تر این روشهاست.


در این پست دنبال رسیدن به این اهداف هستیم:
1-    پیش زمینه ای از مدلسازی موضوعی خواهیم گفت
2-    از الگوریتم LDA استفاده خواهیم کرد. این الگوریتم ریاضیات پیچیده ای دارد که ما واردش نمی شویم اما به صورت خیلی شهودی توضیح میدهیم که این الگوریتم چطور کار می کند.
3-    از کتابخانه topicmodelig در R  استفاده خواهیم کرد.
4-    قدم به قدم جزییات را شرح میدهیم و به منابع دیگری لینک میدهیم، بخصوص درباره مباحثی که جای شرح بیشتر آنها را در این وبلاگ نداریم.


LDA چطور کار می کند؟

در واقع اگر بخواهیم دقیق بدانیم که LDA چطور موضوعات را در یک مجموعه متن شناسایی می کند باید مبنای ریاضیاتی و آماری آن را بررسی کنیم. اما با کنار گذاشتن ریاضیات این مدل هم می توان درکی شهودی از آن بدست آورد.
فرض کنیم مجموعه بزرگی از متن در اختیار داریم. ( بزرگ بودن مجموعه و همین طور کوتاه نبودن متن ها در این روش اهمیت دارد).  LDA  فرض می کند در این مجموعه متن  تعدادی، مثلا 10، موضوع وجود داشته باشد. فرض بعدی این است که هر یک از این موضوعات می تواند سهمی در تشکیل هر یک اسناد موجود در مجموعه متن داشته باشد. یعنی اگر فرض بر این باشد که در مجموع 10 موضوع مختلف داریم. هر یک از اسناد ترکیبی است از تمام این موضوعات ، البته نه با احتمال مساوی! بلکه ممکن است یک یا چند موضوع سهم بیشتری در یک سند داشته باشند.

ولی ما در واقعیت فقط متون و کلمات را می بینیم و نه موضوعات را!  موضوعات در واقع در متن ها پنهان هستند و هدف LDA استخراج این موضوعات پنهان با داشتن متن ها و کلمات است. خوب حالا LDA چطور این موضوعات  پنهان را پیدا می کند؟

 بر اساس جواب به یک سوال در سایت Quora که توسط  ادوین چن  نوشته شده است، LDA به زبان ساده به این ترتیب کار می کند :


  1.  LDA لازم دارد تعداد موضوعات را از به عنوان ورودی دریافت کند. فرض می کنیم تعداد موضوعات k تا باشد. ( یکی از مشکلات LDA همین است!)
  2.  روی مجموعه متن پیمایش کنید و هر کلمه در هر متن را به صورت تصادفی به یکی ازK  تا موضوع نسبت بدهید.
  3.  این نسبت دادن، همین جا یک توزیع احتمالی از کلمات در موضوع ها و یک توزیع احتمال از موضوع ها در متن ها به ما میدهد. (که البته توزیع های خوب و دقیقی نیستند! )
  4.  بنا براین برای بهبود توزی احتمالهای بدست آمده، برای هر متن مراحل زیر را تکرار کنید:
    • برای هر کلمه w در متن مورد نظر مراحل زیر را تکرار کنید
    • برای هر موضوع دو تا چیز را محاسبه کنید
      • 1- کسری از کلمات درمتن d که به موضوع t نسبت داده شده اند. یعنی p(topic t | document d)   را محاسبه کنید
      • 2- نسبتی از همه متن ها که به موضوع t  نسبت داده شده اند به شرطی که کلمه w را داشته باشند. یعنی p(word w| topic t)   را محاسبه کنید.
    • بعد به w  یک تاپیک جدید نسبت بدید و برای این کار موضوع جدید کلمه w را از میان موضوعات موجود به احتمال p(topic t | document d) * p(word w| topic t) انتخاب کنید.  بر اساس مدل generative  ما این دقیقا احتمال اینه که تاپیک t کلمه w را به وجود آورده باشد. بنابراین منطقی  است که موضوع جدید کلمه رو به این روش محاسبه کنیم.
    • در واقع در این مرحله داریم فرض می کنیم که همه موضوع هایی که بههمه کلمات به جز کلمه w نسبت داده ایم به درست هستند. بنابراین با استفاده از مدلی که از توزیع احتمال مرحله قبل داریم موضوع کلمه w رو محاسبه و بعد به روز رسانی می کنیم

بعد از تکرار مراحل بالا به تعداد زیاد به یک وضعیت نسبتا ثابت خواهیم رسید که در آن موضوع هایی که به هر کلمه نسبت داد هایم دیگر تغییر نمی کنند و مدل بدست آمده مدل موضوعی مجموعه متن خواهد بود. 


مدلسازی موضوعی با استفاده از پکیج 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"))


##### remove stop words #####

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)


منابع :
در نوشتن این پست از این پست وبلاگ eight2late خیلی استفاده کرده ام. 
کدها، داده ها و سایر منابع استفاده شده در این پست را می توانید از اینجا بگیرید.
اگر منبع یا پست دیگری که به این بحث مربوط باشد خواندم لینک آن را به اینجا اضافه خواهم کرد.