تکنیک TF-IDF می توان برای استخراج واژه های کلیدی مهم درون یک سند را به کار ببریم تا حس (Sense) و مفهومی از سند بدست آوریم. برای نمونه اگر با صفحه های سایت ویکی پدیا کار می کنیم، با تکنیک TF-IDF می توانیم واژه های کلیدی آن را پیدا کنیم تا پی ببریم زمینه نوشته چیست

بنابراین از TF-IDF می توانیم برای خلاصه سازی یک متن، نوشته یا سند (Document) کمک بگیریم. واژه های کلیدی، یک نوشته را توصیف می کنند. برای نمونه واژه های کلیدی این نوشته می توانند TF-IDF، کتابخانه Scikit-Learn و یادگیری ماشین با پایتون باشند. در این نوشته می خواهیم به کمک دو کلاس CountVectorize و TfidfTransformer واژه های کلیدی درون پست های سایت Stack Overflow را پیدا کنیم. این مجموعه داده ها را به فرمت Json می توانید از اینجا دانلود کنید.

خواندن فایل JSON و ساخت DataFrame

داده هایی که در این نوشته به کار می بریم مجموعه ای از پست های سایت Stack Overflow است که به فرمت JSON در دسترس است. در شکل زیر نخست سه ماژول و کلاس های مورد نیاز را به برنامه پیوست می کنیم. سپس به کمک متد ()read_json از ماژول Pandas و به همراه پارامتر lines=True، فایل JSON را می خوانیم و درون مایه آن را در متغیری به نام dataset نگهداری می کنیم.

در شکل زیر و به کمک دستور dataset.dtypes ساختار دیتافریم نشان داده شده است و می بنید که این دیتافریم دارای ستون هایی به نام accepted_answer_id یا answer_count یا body یا comment_count است که برای ما بیشتر همین ستون body کاربردی است. این ستون دربرگیرنده نوشته های پست با ترکیب تگ های hTML است. همچنین یکی دیگر از ستون های کاربردی برای این نوشته، ستون title است. در شکل زیر و به کمک متد ()head پنج سطر آغازین دیتافریم dataset نشان داده شده است.

تابع pre_process

درون برنامه به یک تابع نیاز داریم تا تگ های HTML درون مقدارهای ستون body را پاک کنیم. برای این باید ماژول re (عبارت با قاعده) را به کار بریم. در کد زیر نخست نوشته ورودی با پارامتر text را به حروف کوچک تبدیل می کنیم و سپس به کمک متد ()sub همه تگ ها از نوشته ورودی حذف شده و در دنباله و دوباره به کمک متد ()sub همه کاراکترهای ویژه از نوشته پاک می شوند.

اکنون باید به کمک تابع pre_process از نو دیتافریم dataset را آماده کنیم به گونه ای که همه تگ ها و کاراکترهای ویژه از درون بدنه (body) و عنوان (title) پاک شوند. همچنین همه حروف به کوچک تبدیل می شوند تا همه واژه های یکسان ولی با حرف های بزرگ و کوچک یکسان در نظر گرفته شوند. در کد زیر و پس از اعمال تابع pre_process بر روی ستون های body و title، یک ستون تازه به نام text به دیتافریم افزوده شده است

شمارش هر یک از واژگان درون نوشته

اکنون باید به کمک کلاس CountVectorizer می بایست شمار (تعداد) هر یک از واژگان را بدست آوریم. در کد زیر تابعی به نام get_stop_words نوشته ایم که نام و مسیر یک فایل متنی ساده را دریافت می کند که این فایل متنی ساده نقش StopWord را پیاده سازی کرده است. این تابع به کمک متد ()open و ()readlines همه خط های فایل را خوانده و سپس در یک حلقه for خط به خط فایل را خوانده و سپس یک مجموعه، یعنی بدون عضو تکراری به کمک متد ()set می سازد. در پایان این مجموعه به نام stop_set را به کمک متد ()frozenset از یک شی تکرارپذیر (Iterable) را به یک شی غیر قابل تغییر (Immutable) تبدیل می کند.

بنابراین با دستور (“stopwords=get_stop_words(“../data/stopwords.txt، یک فهرست از واژگان Stop Word غیر قابل تغییر پذیر داریک که همه عضوهای آن یکتا (منحصر به فرد) هستند. اکنون با دستور ()docs=dataset[‘text’].tolist یم متغیر به نام docs می سازیم که در واقع یک لیست از درون مایه ستون text در دیتافریم dataset است.

پارامترهای max_df و stop_words در کلاس CountVectorizer

با دستور (cv=CountVectorizer(max_df=0.85, stop_words=stopwords یک نمونه از کلاس CountVectorizer ساخته ایم که دو پارامتر تازه را در آن ارزش دهی کرده ایم. پارامتر max_df برای نادیده گرفتن برخی از واژگان است. چون در تابع سازنده کلاس CountVectorizer شماره 0.85 را نشان داده ایم، پس آن واژگانی نادیده گرفته می شوند که در ۸۵ در صد از متن آمده اند. چرایی اینکه این پارامتر را به کار برده ایم برای این است که شاید این واژگان مانند the یا than یا that اهمیت کمتری دارند.

مفهوم Stop Word

پارامتر دیگر در تابع سازنده کلاس CountVectorizer که در کد بالا ارزش دهی شده است، stop_words نام دارد که یک لیست را دریافت می کند. Stop Word مفهومی است در پردازش زبان طبیعی (NLP) یا متن کاوی که یک لیستاز واژگان رایج در هر زبانی مانند انگلیسی یا پارسی است. اگر ما واژگانی را که بسیار تکرار شده اند را از نوشته نادیده بگیریم، بنابراین بر روی واژگان پراهمیت تمرکز می کنیم. Stop Word فهرستی از واژگانی است که پیش از پردازش زبان طبیعی، فیلتر می شوند. برای نمونه فایل Stop Word که در این نوشته به کار می بریم به گونه دستی فهرستی از واژگان پر تکرار در نوشته های انگلیسی را درون خود نگه می دارد که فهرست زیر برخی از آنها را نشان می دهد.

بنابراین متغیر stopwords که خروجی تابع get_stop_words است را به پارامتر stop_words تابع سازنده کلاس CountVectorizer فرستاده ایم که این متغیر فهرستی از واژگان پر تکرار زبان انگلیسی است. به جای این کار می توانستیم از ویژگی های Scikit-Learn نیز کمک بگیریم، به گونه ای که به جای نام متغیر رشته ‘english’ را به پارامتر stop_words بفرستیم. در کدهای زیر docs که به تابع ()fit_transform فرستاده شده است، پیش از این به کمک دستور ()docs=df_idf[‘text’].tolist ساخته شده بود.

بنابراین _cv.vocabulary یک دیکشنری است که هر کلید آن یک واژه درون نوشته و مقدار هر کلید نیز شمار (تعداد) تکرار آن واژه در نوشته است. توجه کنید با توجه به پارامتر max_df که ۰.۸۵ بود، بسیاری از واژگان پر تکرار و کمتر مهم نادیده گرفته شده اند. ساختار یا Shape متغیر word_count_vector به گونه (127672, 20000) است و چرایی (دلیل) این است که فایل JSON ما دارای ۲۰۰۰۰ سطر (یا ۲۰۰۰۰ پست سایت Stack Overflow) بوده و همچنین متغیر _cv.vocabulary نیز دارای ۱۲۷۶۷۲ آیتم (یا زوج های نام واژه – شماره تکرار) است.

بدست آوردن IDF با کلاس TfidfTransformer

از کلاس TfidfTransformer برای بدست آوردن IDF (یا Inverse Document Frequency) از روی خروجی کلاس CountVectorizer کمک می گیریم. همانگونه که در شکل زیر می بینید، متغیر word_count_vector به متد ()fit از کلاس TfidfTransformer فرستاده شده است که این متغیر خروجی کلاس CountVectorizer است. فهرست (یا list) مقدارهای IDF بدست آمده درون متغیر tfidf_transformer نگهداری می شوند. با دستور _tfidf_transformer.idf و در کد زیر، ۱۵ آیتم نخستین درون متغیر را نشان داده ایم.

توجه کنید تفاوت دو کلاس TfidfVectorizer و TfidfTransformer در این است که کلاس TfidfVectorizer ساخت IDF را یکباره و بی نیاز از کلاس CountVectorizer انجام می دهد ولی برای ساخت یا بدست آوردن مقدارهای IDF با کلاس TfidfTransformer، نیاز داریم تا پیش از این فهرست Vocabularity با کلاسی مانند CountVectorizer ساخته شده باشد. بنابراین TfidfVectorizer یعنی نخست CountVectorizer و سپس TfidfTransformer

بدست آوردن TF-IDF و استخراج واژه های کلیدی

پیش از این ما با مجموعه داده عظیمی به نام stackoverflow_data_idf.josn کار کردیم واین فایل را در آغاز برنامه با متد ()pd.read_json خواندیم. اکنون نوبت آن است تا به کمک مجموعه داده سبکتری به نام stackoverflow_test_idf.josn که دارای ۵۰۰ پرسش (یا ۵۰۰ سطر یا پست در سایت Stack Overflow) است، واژگان کلیدی را استخراج کنیم. توجه کنید که متغیر dataset یک دیتافریم است که داده های اصلی را در خود نگه می دارد ولی test_dataset دادهای فایل stackoverflow_test_idf.json را در خود نگه می دارد.

در کد بالا کار تازه ای انجام نداده ایم بلکه همانند پیشین، نخست فایل stackoverflow-test-idf.json را خوانده ایم و سپس به کمک اعمال متد ()pre_process بر روی آن، تگ ها و کاراکترهای ویژه را از مقدارهای ستون های body و title پاک کرده و سپس برایند (نتیجه) را درون کلید تازه ای به نام text ریخته ایم.

توجه کنید تا بدین جا و بر روی فایل stackoverflow-data-idf.json و به کمک کلاس CountVectorizer (متغیر cv) تکرار واژگان را پیدا کردیم و سپس متغیر word_count_vector را به کلاس TfidfTransformer فرستادیم تا ارزش های IDF را بدست آوریم و سپس مجموعه داده سبک تری به نام stackoverflow-test-idf.json را خواندیم. بنابراین با مجموعه داده stackoverflow-data-idf.json در واقع ارزش های IDF واژگان کلیدی بدست آمدند و سپس می خواهیم با مجموعه داده سبک تر، آزمایش کنیم که واژگان کلیدی درون یک سری داده تازه چیست.

اکنون می خواهیم با داشتن یکی از پست های (سطرهای) درون مجموعه داده آزمایشی stackoverflow-test-idf.json، بدانیم کلید واژه های آن چیست. در کد زیر نخست به کمک دستور ()cv.get_feature_names فهرست همه واژه های بدست آمده از اعمال کلاس CountVectorizer بر روی مجموعه داده اصلی stackoverflow-data-idf.json را در یک متغیر به نام feature_names می ریزیم.

سپس به کمک دستور [doc=docs_test[0 نخستین سطر از مجموعه داده آزمایشی را برمی داریم و در متغیری به نام doc می ریزیم تا واژگان کلیدی آن را پیدا کنیم. اکنون باید تابع ()transform را به کمک دستور (([tf_idf_vector=tfidf_transformer.transform(cv.transform([doc بر روی متغیر doc اعمال می کنیم. توجه کنید tfidf_transformer نام متغیری است که پیش از این از روی کلاس TfidfTransformer ساخته بودیم. شکل زیر نمونه ای از خروجی دو دستور ()cv.get_feature_names و [test_dataset[0 را نشان می دهد.

سپس به کمک دستور (()sorted_items=sort_coo(tf_idf_vector.tocoo تابع دست نویس sort_coo را به متغیر doc اعمال می کنیم. این تابع یک فهرست (list) از تاپل ها را دریافت کرده و سپس آنها را مرتب سازی نزولی (از بیشتر به کمتر) مرتب می کند. این تابع در دنباله نوشته آموزش داده شده است.

در پایان دستور (keywords=extract_topn_from_vector(feature_names, sorted_items, 10 فهرست واژگان کلیدی را به کمک تابع extract_topn_from_vector استخراج می کند. این تابع نام ویژگی ها و فهرستی مرتب شده از مقدارهای IDF را در یافت می کند. آرگومان سوم آن شماره ای است که شمار واژگان کلیدی که استخراج می شوند را نشان می دهد. توجه کنید در برنامه متغیر feature_names فهرست ویژگی ها را در خود دارد. در پردازش متن و پردازش زبان طبیعی ویژگی ها همان واژگان هستند. همچنین فهرست مقدارهای iDF در متغیر tf_idf_vector نگهداری می شود که این متغیر برآیند دستور (([tfidf_transformer.transform(cv.transform([doc را در خود نگه می دارد.

تابع sort_coo برای مرتب سازی دیکشنری ها

در نوشته مرتب سازی دیکشنری ها به طور ویژه درباره چگونگی به کارگیری تابع درونی ()sorted و عبارت های لامبدا را برای مرتب کردن دیکشنری های گفته ایم. تابع sort_coo یک ماتریس (آرایه دوبعدی) را از ورودی دریافت می کند و سپس تابع درونی ()zip را بر روی عناصر coo_matrix.col و coo_matrix.data اعمال می کند. برآیند (نتیجه) برگشتی تابع ()zip یک تاپل است که در متغیر tuples ریخته می شود و سپس این تاپل به کمک متد ()sorted و عبارت لامبدا و به گونه نزولی مرتب سازی می شود.

مرتب سازی لیست و تاپل در پایتون

تابع extract_topn_from_vector برای استخراج واژگان کلیدی

این تابع سه ورودی که به ترتیب نام ویژگی ها یا همان فهرست واژگان شمارش شده با CountVectorizer، فهرستی از تاپل های مرتب شده و شماره ای که شمار (تعداد) واژگان کلیدی برگشتی را دریافت می کند که این شماره پیشفرض برابر با ۱۰ است. سپس به کمک دستور [sorted_items = sorted_items[:topn به اندازه مقدار پارامتر topn، از فهرست مرتب شده برداشته می شود.

در کد زیر می خواهیم دو فهرست تازه ساخته شده به نام های score_vals و feature_vals را در یک حلقه تو در تو و به اندازه متغیر sorted_items پر کنیم. sorted_items فهرستی از تاپل هایی است که هر تاپل دارای دو مقدار است که نخستین آنها در متغیر idx و دومین در score نگهداری می شود. در واقع ارزش هایی که در متغیر score ریخته می شوند، شماره ارزش یا امتیاز TF-IDF برای اندیس idx هستند. اگر دقت کنید می خواهیم هم ارزش TF-IDF و هم با شماره اندیس idx، نام واژه را از متغیر feature_names بدست آوریم و در متغیرهای score_vals و feature_vals بریزیم.

در پایان می خواهیم دوتایی های نام ویژگی و ارزش یا امتیاز TF-IDF را به گونه یک دیکشنری به نام result بدست یک حلقه for بسازیم که در این دیکشنری برگشت داده می شود. برای نمونه مقدار برگشتی این تابع دیکشنری خواهد بود که پیش از این در کدهای بالا در متغیر keywords در دستور (keywords=extract_topn_from_vector(feature_names,sorted_items,10 ریخته شده بود.