فیلم گردی با فیلیمو (بخش دوم: متن کاوی و دسته بندی فیلم ها)

منبع: باشگاه خبرنگاران جوان
منبع: باشگاه خبرنگاران جوان

در بخش اول به کمک مدلسازی گراف رابطه بین بازیگران فیلمها رو بررسی کردیم. در این بخش در دو قسمت مجزا ابتدا با داشبوردی برای جستجو در داده ها آشنا میشیم. سپس بر اساس مدل ساده ای به دسته بندی فیلم ها میپردازیم

قسمت اول

برای دسترسی به داشبورد کلیک کنید

داشبوردی که برای جستجو در داده ها استفاده میکنیم با کمک ابزار محبوب من :) یعنی power bi تهیه شده. جدول پایین داشبورد اطلاعات جامعی از فیلم ها از قبیل کارگردان، لینک دسترسی به فیلم روی سایت فیلیمو ،بازیگران، سال ساخت، معرفی فیلم در اختیارتون قرار میده و دو تا شکل بالای داشبورد به عنوان فیلتر عمل میکنن. نمودار میله ای سمت چپ به کارگردان های مختلف روی محور افقی و تعداد کارهای اونها اشاره داره. به عنوان نمونه منوچهر هادی و هاتف علیمردانی که پرکارترین کارگردان ها هم هستن، هر کدوم 6 فیلم در مجموعه داده ها دارن. با کلیک روی هر کدوم از اونها میتونیم اطلاعات رو بر اساس همون کارگردان فیلتر کنیم.

شکل زیر نشون میده چطور با کلیک روی لینک فیلم به صفحه فیلم روی سایت فیلیمو هدایت میشین

به کمک جدول سمت راست میتونیم بر اساس موارد مختلف از قبیل اسم فیلم، کارگردان، بازیگر و حتی محتوای فیلم جستجو کنیم (شکل زیر جستجو در محتوا بر اساس کلمه پلیس رو نشون میده).

جستجو بر اساس فیلم های شهاب حسینی

در قسمت دوم به طور مختصر از دسته بندی (classification) در یادگیری ماشین و اصطلاحات پردازش متن صحبت میکنیم و برای اجرای کار از زبان پایتون استفاده میکنیم.

قسمت دوم

این بخش به مراحل مختلف اجرای یک مدل دسته بندی برای پیش بینی ژانر فیلم ها بر اساس متن معرفی فیلم اختصاص داره. هرچند توضیحات قسمت معرفی فیلم، خیلی مختصر نوشته شدن و تنوع فیلم در بیشتر ژانرها برای آموزش مدل کافی نیست. ولی همین مراحل در سایر مسائل قابل پیاده سازی هست. به همین دلیل برای ارزیابی فرایند، تمام مراحل رو بر روی مورد مشابهی از سایت دیوار تکرار میکنیم.

دسته بندی (classification) نوعی روش یادگیری ماشین هست که جزیی از روش های با نظارت (supervised learning) محسوب میشه.در روش های با نظارت داده ها دارای برچسب (label) هستن و هدف، رسیدن به الگوی مناسب (توسط داده های پیشین و برچسب هر کدام) برای پیش یبنی برچسب داده های جدید هست.

مساله مورد نظر رو به این صورت طرح میکینم: قصد داریم با در نظر گرفتن متن معرفی فیلم ها به عنوان متغیر ورودی (feature)، ژانر فیلم رو به عنوان متغیر خروجی (target) تشخیص بدیم.

import pandas as pd
df=pd.read_csv('filimo.csv',encoding='utf-8')
df

ابتدا پراکندگی ژانرها رو برررسی میکنیم

df['genres'].value_counts().plot(kind='barh')

بیشترین فروانی مربوط به ژانر خانوادگی اجتماعی هست که به تنهایی تقریبا نیمی از داده ها رو شامل میشه (تعداد فیلم ها= 360).

برای اینکه بتونیم از داده های متنی به صورت متغیرهای ورودی (feature) استفاده کنیم باید به طریقی از فرمت متنی به فرمت عددی تبدیل کنیم. برای این کار تکنیک های زیادی وجود داره مثل:

-bag of words

-word vectors

-tfidf

صندوقچه کلمات (bag of words) تکنینکی هست که در این پست استفاده میکنیم. در این روش هر کلمه ای که در مجموعه داده ها قرار داره به عنوان متغیر ورودی (feature) در نظر گرفته میشه. مفهوم صندوقچه کلمات به مجموعه متغیرهای ورودی اشاره داره. به این صورت که هر نمونه (sample) در مجموعه که در اینجا متن معرفی فیلم هست، به کلمات سازنده تجزیه میشه و کلماتی که قبلا در صندوقچه کلمات وجود نداشتن (غیر تکراری) به مجموعه اضافه میشن. در نهایت مجوعه ای از تمام کلمات موجود در داده ها به عنوان متغیرهای ورودی خواهیم داشت.

با توجه به شرایط مساله ما، حضور و یا عدم حضور کلمه در هر نمونه اهمیت داره اما تعداد تکرار کلمات اهمیت چندانی نداره. مثلا وجود یا عدم وجود کلمه پلیس، کافیه تا مشخص بشه ژانر فیلم پلیسیه و تعداد تکرار ملاک نیست. در این مواقع ساده ترین روش مشابه شکل بالا انتخاب حالت باینری برای متغیرهاست. حضور یا عدم حضور کلمه (term) در نمونه (document) مشخص کننده مقدار 0 و1 برای متغیرهاست.
نکته دیگه در مورد داده های متنی نرمال سازی (normalization) و لماتایز (lemmatization) کلمه هاست. نرمال سازی یعنی یکسان سازی کلمات از نظر نگارشهای مختلف. مثلا نگارش کلمه کتابها و کتاب ها نباید باعث بشه پردازش متفاوتی روی کلمات صورت بگیره و نباید این دو نوع نگارش باعث ایجاد دو متغیر در صندوقچه کلمات بشه. یکی از این دو نوع نگارش مبنا قرار میگیره و سایر نگارش ها یکسان سازی میشه. لماتایز مشابه نرمالسازی باعث یکسان سازی کلمات میشه با این تفاوت که کلمات هم ریشه یکسان سازی میشن. مثلا دو کلمه دستم و دستش، تنها در ضمیر متصل تفاوت دارن و معناهای متفاوتی ندارن و باید اونها رو یکسان سازی کنیم.

توضیح اضافه: اگه قبلا با مفهوم مشابهی به نام stemming (یکی دیگه از روش های ریشه یابی) آشنا باشید، میدونید که عبارت حاصل از عمل lemmatization معنی دار هست در صورتی که در مورد stemming الزما ابن طور نیست.

پردازش هایی که در مورد داده های متنی اشاره کردیم، برای زبان انگلیسی ابزارهای متنوعی دارن. معروف ترین اونها spacy و nltk هستن. برای زبان فارسی در پایتون، هضم تنها ابزار موجوده که به صورت عمومی عرضه شده. هر چند ابزارهایی به صورت وب سرویس وجود دارن و در حال توسعه هستن (مثل واکاویک و text-mining).

به کمک هضم پردازشهای مورد نیاز رو انجام میدیم و ستون جدیدی (processed) برای داده های پردازش شده ایجاد میکنیم.

normalizer = Normalizer()
lemmatizer = Lemmatizer()
stemmer=Stemmer()
def processor(x):
     s=''"
     x=re.sub(r'\(|\)|،|\.',' ',x)
     normed=normalizer.normalize(x)
     for i in word_tokenize(normed):
         s=s+" "+lemmatizer.lemmatize(i)
     return s
 df['processed']=df.desc.apply(processor)

مقایسه سطر اول داده ها، قبل از پردازش(desc) و بعد از پردازش (processed):

print(df.loc[0,'desc'])
print(df.loc[0,'processed'])

output:
در فیلم گام های شیدایی ، جولیا به خدمت ارتش ایالات متحده درآمده و تلاش دارد مداركی كه در حین انجام وظیفه در این كشور به دست آورده به دوست خبرنگار خود برساند. در جریان این اتفاقات یكی از پرده های سناریو آزادسازی عراق به تصویر كشیده می شود....
در فیلم گام شیدا جولیا به خدمت ارتش ایالات متحده درآمده و تلاش داشت#دار مدارک که در حین انجام وظیفه در این کشور به دست آورده به دوست خبرنگار خود رساند#رسان در جریان این اتفاقات یک از پرده سناریو آزادسازی عراق به تصویر کشید#کش

متغیرهای ورودی (X) و خروجی (y) رو انتخاب میکنیم و داده های آموزش و تست رو جدا میکنیم:

X=df['processed']
y=df['genres']
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.2, random_state = 0)

داده های متنی رو به کمک تکنیک صندوقچه کلمات به فرمت عددی تبدیل میکنیم.

from sklearn.feature_extraction.text import CountVectorizer
with open("Persian_StopList.txt",encoding='utf-8') as file:
    stop=[i.strip() for i in file]
count_vect = CountVectorizer(stop_words=stop,binary=True)
X_train_tf = count_vect.fit_transform(X_train)

نکته قابل اشاره در تکه کد بالا کلمات ایست (stop words) هستن. کلمات ایست، به کلماتی گفته میشه که بار معنایی خاصی در متن ندارن. کلماتی مثل(از ، در، با، برای و...). در مدلسازی این کلمات معمولا به عنوان متغیرهای زاید حذف میشن. این کلمات بسته به زمینه مطالعه و کاربرد ممکنه متفاوت باشن. اگه به دسته بندی فیلمها برگردیم، در متن معرفی فیلم ها کلماتی مثل بازیگر،فیلم و... وجود دارن که کمکی به تشخیص ژانر فیلم نمیکنن یا به عبارت دیگه نمیتونیم از روی این کلمات ژانر فیلم رو حدس بزنیم(کاری که مدل ما قراره انجام بده، دقیقا مشابه همینه). حذف این کلمات معمولا کارایی مدل رو افزایش میده.

بعد از پیش پردازش داده ها نوبت به آموزش و اجرای مدل میرسه. یکی از الگوریتم هایی که برای دسته بندی متن مناسبه Naive Bayes هست. این مدل بر اساس احتمالات شرطی (conditional probability) کار میکنه و بر اساس کلمات هر نمونه و دسته های مختلف (ژانرهای مختلف) دسته با بیشترین احتمال رو پیش بینی میکنه. بعد از انجام تمام مراحل پیش پردازش در نهایت مدلسازی رو انجام میدیم.

from sklearn.naive_bayes import BernoulliNB
nb=BernoulliNB(binarize=None)
nb.fit(X_train_tf,y_train)

داده های تست (X_test_tf) رو باتوجه به صندوقچه کلمات آماده میکنیم. دقت مدل (نسبت پیش بینی های درست به کل پیش بینی ها) به این صورت به دست میاد.

X_test_tf = count_vect.transform(X_test)
nb.score(X_test_tf,y_test)
output:  0.6197183098591549

برای سنجش مدل نیاز به شاخص هایی داریم. یکی از شاخص ها برای این کار، مقایسه با dummy classifier هست. در این روش مدلی بر اساس ویژگی های داده ها به دست میاد که بر اساس فرض های ساده به وجود اومده. با استفاده از تکه کد زیر مدلی میسازیم که بر اساس پرتکرارترین ژانر کار میکنه (به پارامتر strategy="most_frequent" دقت کنید). قبلا دیدیم که ژانر خانوادگی، اجتماعی بیشترین فراوانی رو در بین سایر ژانرها داره. مدل زیر بر مبنای همین فرض کار میکنه و برچسب تمام داده های تست رو (خانوادگی، اجتماعی) در نظر میگیره (بدون اینکه طبق متغیرهای ورودی آموزش ببینه). اما نکته جالب اینجاست که دقیقا همون دقت مدل naive bayes رو به دست میاره.

from sklearn.dummy import DummyClassifier
dummy=DummyClassifier(strategy="most_frequent")
dummy.fit(X_train_tf,y_train)
X_test_tf = count_vect.transform(X_test)
dummy.predict(X_test_tf)
 
 output:
array(['خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی',
       'خانوادگی,اجتماعی', 'خانوادگی,اجتماعی'], dtype='<U16')
dummy.score(X_test_tf,y_test)
output: 0.6197183098591549

پس مدل ما (naive bayes) از نظر فنی فاقد ارزشه. راه حلهای مختلفی برای بهبود مدل وجود داره. مثل انتخاب سایر الگوریتم های دسته بندی (که در این مورد خاص بهبود خاصی ایجاد نشد)، جمع آوری داده های بیشتر(در صورت امکان) و...

با نگاه دقیق تر به مجموعه داده ها به نظر میرسه توضیحات معرفی فیلم برای تشخیص ژانر کافی نباشه. هرچند با افزایش حجم داده ها احتمالا دقت مدل افزایش پیدا میکنه.

بد نیست همین مدل و فرایند رو در مورد مجموعه داده های بزرگتر (حدود یک میلیون آگهی) و احتمالا دقیق تر سایت دیوار بررسی کنیم.

داده های سایت دیوار شامل اطلاعات متنوعی هستن. ستون های مورد نظر ما، متن آگهی و دسته مورد نظر آگهی هاست.

divar[['cat1','desc']]
متن آگهی (desc) و دسته آگهی(cat1)
متن آگهی (desc) و دسته آگهی(cat1)

فراوانی دسته های مختلف:

divar['cat1'].value_counts().plot(kind='barh')

تمام مراحل پیش پردازش شامل حذف کلمات ایست، normalization ، lemmatization و صندوقچه کلمات دقیقا مشابه قبل انجام میشه. در نهایت مدلسازی با الگوریتم naive bayes انجام میشه و دقت مدل با dummy classifier مقایسه میشه.

X=divar['processed']
y=divar['cat1']
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.2)
count_vect = CountVectorizer(stop_words=stop,binary=True)
X_train_tf = count_vect.fit_transform(X_train)

nb=BernoulliNB(binarize=None)
nb.fit(X_train_tf,y_train)
X_test_tf = count_vect.transform(X_test)
nb.score(X_test_tf,y_test)
output:  
0.856095343123029

مقایسه با dummy classifier (مدلی که برای تمام نمونه ها پرتکرار ترین دسته (for-the-home) رو در نظر میگیره)

from sklearn.dummy import DummyClassifier
dummy=DummyClassifier(strategy="most_frequent")
dummy.fit(X_train_tf,y_train)
X_test_tf = count_vect.transform(X_test)
dummy.score(X_test_tf,y_test)
output: 
0.35734323911234933

همون طور که خروجی های بالا نشون میده مدلی که روی داده های سایت دیوار آموزش دیده دقت خوبی داره (دقت حدود 85 درصد) و نسبت به dummy classisfier برتری داره (دقت حدود 35 درصد). بررسی دو نمونه مختلف به خوبی نشون داد محدودیت های استفاده از مدل ها چطور نمود پیدا میکنن و در صورت عدم وجود داده های مناسب و به تعداد کافی نمیشه انتظار معجزه داشت.

دسته بندی متن یکی از حوزه های پرکاربرد تحلیل داده محسوب میشه. مسائلی مثل شناسایی اسپم (spam detection)، تحلیل احساسات، اتوماسیون کارها و... میتونه بخشی از کابردهای این حوزه باشه. سپاس از وقتی که برای حوندن این پست صرف کردین. امیدوارم براتون مفید باشه.

ایمیل: pooryaganji1368@gmail.com

توییتر

لینکدین

اینستاگرام