به نام خدا


تحلیل داده‌های متن


مرتب‌سازی داده


چرا باید داده را مرتب کنیم؟


هنگامی که با تحلیل داده سر و کار داریم، اکثر مواقع داده‌ی در دست، داده‌ای نامرتب و به هم‌ریخته است. در قدم اول کاری که تحلیل را آسان‌تر و کاراتر می‌کند تمیز کردن و ساختار دادن به داده است. در کار با داده‌های متنی نیز ابتدا باید آن‌ را به نحوی مرتب کنیم که نیازهای ما را برآورده کند.

این مقاله داده‌ی مرتب را داده‌ای با ویژگی‌های زیر تعریف می‌کند:
  1. هر متغیر یک ستون است.
  2. هر مشاهده یک سطر است.
  3. هر مدل از واحد مشاهده یک جدول است.

برای داده‌های متنی، مدل تمیز شده را جدولی با یک token در هر سطر تعریف می‌کنیم. یک token، واحد معناداری از متن است (مثلا یک کلمه یا یک جمعه) که ما برای تحلیل از آن استفاده می‌کنیم. در مرتب‌سازی داده‌های متنی، تلاش می‌کنیم که طی فرآیند tokenization متن را به tokenها تقسیم کنیم.

پس از این مقدمه و تعاریف، پیاده‌سازی این عملیات در R را شروع می‌کنیم.


نصب کتاب‌خانه‌ی tidytext


با استفاده از یکی از دو روش زیر، کتابخانه‌ی tidytext را نصب کنید.
In [ ]:
# روش اول
install.packages("tidytext")

# روش دوم
library(devtools)
install_github("juliasilge/tidytext")
پس از اتمام فرآیند نصب، با اجرای دستور زیر می‌توانید از آن استفاده کنید.
In [1]:
library(tidytext)


تابع unnest_tokens


داده‌ی ساده‌ي زیر شامل چند بیت آغاز شاهنامه‌ی فردوسی است.
In [3]:
library(tidyverse)
df <- data_frame(text = c("به نام خداوند جان و خرد", 
       "کزین برتر اندیشه برنگذرد",
       "خداوند نام و خداوند جای",
       "خداوند روزی ده رهنمای"))
می‌خواهیم این دیتافریم را طبق تعریفی که قبلا ارائه کردیم tokenize کنیم. تابع $\texttt{unnest_tokens()}$ به شکل بسیار راحتی این کار را برای ما می‌کند. این تابع ورودی‌های زیر را می‌گیرد:
  1. دیتافریمی که باید tokenize کند.
  2. نام ستون خروجی شامل tokenها
  3. نام ستون ورودی شامل متن
  4. نوع token
تا اینجا مورد اول تا سوم برای ما کافی هستند، در رابطه با مورد چهارم جلوتر توضیح خواهیم داد. مطابق با توضیحات بالا، تابع را فراخوانی می‌کنیم
In [4]:
df %>% unnest_tokens(word, text)
word
به
نام
خداوند
جان
و
خرد
کزین
برتر
اندیشه
برنگذرد
خداوند
نام
و
خداوند
جای
خداوند
روزی
ده
رهنمای
می‌بینید که در ستون خروجی word، کلمات جداشده‌ی متن قرار دارند.


یک قدم فراتر: بررسی چند بخش شاهنامه


داده‌ي موجود در فایل sample.txt شامل ۱۰ بخش اول داستان رستم و اسفندیار شاهنامه است. با دستور زیر داده را می‌خوانیم.
In [5]:
rostam_esfandiar <- read.delim(file = "sample.txt", stringsAsFactors = F, header = F)
colnames(rostam_esfandiar) = "beyt"
پس از بررسی داده، متوجه می‌شویم که شروع هر بخش با «*بخش n*» مشخص شده است که n عدد با فونت فارسی است. می‌خواهیم بخش‌های مختلف کتاب را از هم جدا کنیم. به regex زیر توجه کنید:
In [6]:
regex_chapter <- "بخش [۰۱۲۳۴۵۶۷۸۹]+"
این regex تشخیص می‌دهد که «بخش n» در متن قرار دارد یا نه. برای اطلاعات بیشتر از regexها به اینجا و اینجا مراجعه کنید.

با استفاده از دستورات زیر مشخص می‌کنیم که هر بیت مربوط به کدام بخش است:
In [7]:
library(stringr)
rostam_esfandiar <- rostam_esfandiar %>%
    mutate(chapter = cumsum(str_detect(beyt, regex_chapter)))
rostam_esfandiar %>% sample_n(10)
beytchapter
188بدیدی همی تیغ ارجاسپ را 3
771زواره نخستین دمی درکشید 9
90که فرزند جویندهٔ گاه شد 2
244چه پیچان همانا که بیجان شود 4
354همانا چو سهراب دیگر سوار 5
387ز من نشنود سرد هرگز سخن 5
768دگر جام بر دست بهمن نهاد 9
158چو در پیش او انجمن شد سپاه 3
269چنین پاسخ آوردش اسفندیار 4
93بخواند آن زمان شاه جاماسپ را2
مانند فرآیندی که در بخش قبل انجام دادیم، کلمات را جدا کرده، تعداد آن ها در هر فصل را بدست می‌آوریم و به ترتیب نزولی می‌چینیم.
In [8]:
rostam_esfandiar <- rostam_esfandiar %>% 
    unnest_tokens(word, beyt) %>% 
    group_by(chapter, word) %>% 
    summarise(count = n()) %>% 
    arrange(chapter, desc(count))
rostam_esfandiar %>% head(10)
chapterwordcount
1 همی 10
1 از 7
1 و 7
1 به 6
1 که 6
1 بلبل 5
1 گل 5
1 ابر 4
1 چو 4
1 باد 3
در هر بخش ۱۰ کلمه‌ی پر‌تکرار را انتخاب می‌کنیم
In [9]:
rostam_esfandiar.top10 <- rostam_esfandiar %>% 
  mutate(rank = rank(-count) %>% as.integer(), 
         rank = row_number(rank)) %>% 
  filter(rank <= 10)
در نهایت نمودار فراوانی این کلمات را رسم می‌کنیم
In [10]:
p <- ggplot(rostam_esfandiar.top10, aes(x = reorder(word, -count), y = count, fill = as.factor(paste("بخش", chapter, sep = " ")))) + 
  geom_bar(stat = "identity", show.legend = FALSE) + 
  facet_wrap(~chapter, ncol = 5,  scales = "free") + 
  theme_minimal() + 
  theme(axis.title.x = element_blank(), axis.title.y = element_blank()) + 
  coord_flip()
چون ggplot برای زبان‌های راست به چپ درست عمل نمی‌کند از کتابخانه‌ی plotly استفاده می‌کنیم.
In [12]:
library(plotly)
ggplotly(p)


تحلیل متن با استفاده از فراوانی


تعریف معیار tf-idf


اولین معیاری که برای میزان مهم بودن یک کلمه به ذهن می‌رسد این است که تعداد دفعات استفاده از آن را در نظر بگیریم. ولی در قسمت قبل دیدیم که با این کار تعداد زیادی کلمات بی‌معنی به عنوان کلمات مهم انتخاب می‌شوند. شاید وسوسه شویم که لیستی از کلمات اضافه و بی‌معنی تعریف کنیم و این کلمات را از متن حذف کنیم. ولی این کار، حرفه‌ای نیست و لزوما نتایج درستی به همراه نخواهد داشت.

فرض کنید میخواهیم کلمات مهم هر فصل‌ شاهنامه را استخراج کنیم. در واقع ما به دنبال کلماتی هستیم که علاوه بر اینکه در یک فصل زیاد تکرار شده‌اند در فصول دیگر زیاد استفاده نشده باشد. بدین منظور دو معیار زیر را تعریف می‌کنیم:
  • معیار $\text{TF(Term Frequency)}$: تعداد دفعات تکرار این کلمه در مستند به تعداد کل کلمات مستند

    $$tf(term) = \frac{n_{term~in~document}}{n_{total~ document~words}}$$


  • معیار $\text{IDF(Inverse Document Frequency)}$: لگاریتم طبیعی معکوس تعداد مستندات شامل این کلمه به تعداد کل مستندات. $$idf(term) = ln\Big(\frac{n_{documents}}{n_{documents~containing~term}}\Big)$$

معیار tf-idf را ضرب این دو عدد تعریف می‌کنیم.


پیاده‌سازی tf-idf در R


تابع $\texttt{bind_tf_idf}$ در کتابخانه‌ی tidytext این معیار‌ها را به سادگی محاسبه می‌کند. این تابع ورودی‌های زیر را می‌گیرد:
  1. دیتافریمی که باید بررسی کند.
  2. نام ستون ورودی شامل tokenها
  3. نام ستون ورودی شامل نام مستندات
  4. نام ستون ورودی شامل تعداد دفعاتی که هر token در مستند مربوطه تکرار شده‌است.
می‌خواهیم عملکرد این تابع را بررسی کنیم. دیتافریم زیر، شامل ۱۰ بخش اول داستان‌های رستم و اسفندیار، سهراب، ضحاک، سیاوس و پادشاهی بهرام گور در شاهنامه‌ي فردوسی است.
In [14]:
shahname <- read_csv("shahname.csv")
shahname %>% head(5)
Parsed with column specification:
cols(
  text = col_character(),
  book = col_character()
)
textbook
*بخش ۱* bahram
چو بر تخت بنشست بهرام گور bahram
برو آفرین کرد بهرام و هور bahram
پرستش گرفت آفریننده را bahram
جهاندار و بیدار و بیننده راbahram
مطابق بخش اول، متن را tokenize می‌کنیم، تعداد دفعات تکرار هر کلمه در هرکتاب را بدست می‌آوریم. می‌بینید که کلماتی که از همه بیشتر تکرار شده‌اند بی‌معنی‌اند.
In [15]:
shahname.words <- shahname %>% 
    unnest_tokens(word, text) %>%
    group_by(book, word) %>% 
    summarise(count = n())
shahname.words %>% arrange(book, desc(count)) %>% head(5)
bookwordcount
bahramو 288
bahramبه 235
bahramکه 130
bahramز 98
bahramاز 96
حال از تابع $\texttt{bind_tf_idf}$ استفاده می‌کنیم. می‌بینید که کلمات با tf-idf بالاتر از لحاظ منطقی بامعنا‌ترند.
In [16]:
shahname.words <- shahname.words %>% 
    bind_tf_idf(word, book, count) %>% 
    arrange(book, desc(tf_idf))
shahname.words %>% head(10)
bookwordcounttfidftf_idf
bahram لنبک 14 0.0023565061.6094379 0.003792650
bahram بهرام 43 0.0072378390.5108256 0.003697273
bahram راهام 12 0.0020198621.6094379 0.003250842
bahram آبکش 10 0.0016832181.6094379 0.002709035
bahram مه 13 0.0021881840.9162907 0.002005013
bahram روزبه 7 0.0011782531.6094379 0.001896325
bahram خانه 22 0.0037030800.5108256 0.001891628
bahram جام 20 0.0033664370.5108256 0.001719662
bahram جهود 6 0.0010099311.6094379 0.001625421
bahram خر 6 0.0010099311.6094379 0.001625421
در نهایت نمودار ۱۰ کلمات کلیدی هر فصل را رسم می‌کنیم.
In [17]:
plot.data <- shahname.words %>% 
  mutate(rank = rank(-tf_idf) %>% as.integer(), 
         rank = row_number(rank)) %>% 
  filter(rank <= 10) %>% arrange(book, rank)
In [18]:
p <- ggplot(plot.data, aes(x = reorder(word, -tf_idf), y = tf_idf, fill = book)) + 
  geom_bar(stat = "identity", show.legend = FALSE) + 
  facet_wrap(~book, ncol = 2,  scales = "free") + 
  theme_minimal()
In [19]:
ggplotly(p)


تحلیل متن با استفاده از رابطه‌ی بین کلمات


تعریف n-گرام


تا به حال کل کاری که برای تحلیل متن کردیم بررسی تک‌ تک کلمات یا رابطه‌شان با کل مستند بود. در حالی که برای تحلیل کامل و جامع تنها این کافی نیست. واضح است که کلمات بین خودشان نیز با هم رابطه دارند.
واژه‌ی n-گرام به معنای کلمات nتایی متوالی موجود در متن است. برای مثال به مصراع زیر توجه کنید:
بسی رنج بردم در این سال سی
عبارات «بسی رنج بردم»، «رنج بردم در»، «بردم در این»، «در این سال» و «این سال سی» ۳-گرام‌های آن هستند.


بدست آوردن n-گرام‌ها در R


حال نوبت آن رسیده است که ورودی چهارم تابع $\texttt{unnest_tokens()}$، یعنی نوع tokenها را مورد بررسی قرار دهیم.

برای اینکه بتوانیم n-گرام‌ها را با استفاده از این تابع بدست آوریم، باید علاوه بر ورودی‌های قبلی، گزینه‌ی$\texttt{token = “ngrams”}$ را نیز به آن اضافه کنیم. هم‌چنین ورودی n در این تابع، بیانگر طول n-گرام‌هاست.
برای مثال سعی می‌کنیم ۲-گرام‌های متون شاهنامه‌ که در قسمت قبل نیز استفاده کردیم را بدست آوریم:
In [20]:
shahname.bigrams <- shahname %>% 
    unnest_tokens(bigram, text, token = "ngrams", n = 2)

shahname.bigrams %>% head(5)
bookbigram
bahram بخش ۱
bahram ۱ چو
bahram چو بر
bahram بر تخت
bahram تخت بنشست
مانند قبل آن ها را بر حسب تعداد دفعات تکرار در هر فصل مرتب می‌کنیم:
In [21]:
shahname.bigrams <- shahname.bigrams %>% 
    group_by(book, bigram) %>% 
    summarise(count = n()) %>% 
    arrange(book, desc(count)) 

shahname.bigrams %>% arrange(book, desc(count)) %>%head(5)
bookbigramcount
bahram بدو گفت 23
bahram چنین گفت 18
bahram به راهام 12
bahram گفت بهرام12
bahram گفت کاین 10
برخلاف تک‌کلمات که خیلی بی‌معنی بودند، این کلمات نسبتا با معنی‌ترند.

با استفاده از تابع $\texttt{separate}$ موجود در کتابخانه‌ی tidyr می‌توان این دو کلمه را از هم جدا کرد.
In [22]:
#3
shahname.bigrams %>% 
    separate(bigram, c("word1", "word2"), sep = " ") %>% 
    head(5)
bookword1word2count
bahramبدو گفت 23
bahramچنین گفت 18
bahramبه راهام 12
bahramگفت بهرام 12
bahramگفت کاین 10
در صورتی که از این تابع استفاده کردید و پس از آن خواستید مجدد این دو کلمه را به هم بچسبانید می‌توانید، از تابع $\texttt{unite()}$ استفاده کنید.


تحلیل n-گرام‌ها با استفاده از tf-idf


همانند تک‌کلمات که قبلا با tf-idf تحلیل کردیم، n-گرام‌ها را نیز می‌توان با این روش تحلیل کرد.
In [23]:
shahname.bigrams <- shahname.bigrams %>% 
    bind_tf_idf(bigram, book, count) %>% 
    arrange(book, desc(tf_idf))

shahname.bigrams %>% head(5)
bookbigramcounttfidftf_idf
bahram به راهام 12 0.0020202021.609438 0.003251390
bahram گفت بهرام 12 0.0020202021.609438 0.003251390
bahram بهرام گور 8 0.0013468011.609438 0.002167593
bahram و چارپای 7 0.0011784511.609438 0.001896644
bahram به بهرام 6 0.0010101011.609438 0.001625695
نمودار ۱۰تای برتر را رسم می‌کنیم.
In [24]:
plot.data <- shahname.bigrams %>% 
    mutate(rank = rank(-tf_idf) %>% as.integer(), rank = row_number(rank)) %>% 
    filter(rank <= 10) %>% 
    arrange(book, rank)
In [25]:
p <- ggplot(plot.data, aes(x = reorder(bigram, -tf_idf), y = tf_idf, fill = book)) + 
  geom_bar(stat = "identity", show.legend = FALSE) + 
  facet_wrap(~book,scales = "free") + 
  theme_minimal() + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1), 
        axis.title.x = element_blank(), axis.title.y = element_blank())
In [26]:
ggplotly(p)