किनोपोइक से समीक्षाओं के भावनात्मक रंग का विश्लेषण

प्रविष्टि


प्राकृतिक भाषा प्रसंस्करण (एनएलपी) मशीन सीखने का एक लोकप्रिय और महत्वपूर्ण क्षेत्र है। इस हब में, मैं पायथन में लिखित फिल्म समीक्षाओं के भावनात्मक रंगांकन के विश्लेषण से संबंधित अपनी पहली परियोजना का वर्णन करूंगा। भावुक विश्लेषण का कार्य उन लोगों के बीच काफी आम है जो एनएलपी की बुनियादी अवधारणाओं में महारत हासिल करना चाहते हैं, और इस क्षेत्र में 'हैलो दुनिया' का एक एनालॉग बन सकते हैं।

इस लेख में, हम डेटा साइंस प्रक्रिया के सभी मुख्य चरणों से गुजरेंगे: अपने स्वयं के डेटासेट बनाने से लेकर, इसे संसाधित करने और एनएलटीके लाइब्रेरी का उपयोग करने वाले फीचर्स को निकालना, और आखिरकार स्किट-लर्न का उपयोग करके मॉडल को सीखना और ट्यूनिंग करना। कार्य स्वयं तीन वर्गों में समीक्षाओं को वर्गीकृत करना है: नकारात्मक, तटस्थ और सकारात्मक।

डेटा कॉर्पस फॉर्मेशन


इस समस्या को हल करने के लिए, कोई IMDB की समीक्षाओं के साथ कुछ तैयार-निर्मित और एनोटेट डेटा बॉडी का उपयोग कर सकता है, जिनमें से कई GitHub पर हैं। लेकिन यह किन्नोपोइक से ली गई रूसी में समीक्षाओं के साथ अपना खुद का बनाने का निर्णय लिया गया था। उन्हें मैन्युअल रूप से कॉपी न करने के लिए, हम एक वेब पार्सर लिखेंगे। मैं HTML फ़ाइलों को संसाधित करने के लिए http अनुरोधों और ब्यूटीफुलसप को भेजने के लिए अनुरोध लाइब्रेरी का उपयोग करूंगा। सबसे पहले, एक फ़ंक्शन को परिभाषित करें जो मूवी समीक्षाओं के लिए लिंक लेगा और उन्हें पुनर्प्राप्त करेगा। हम में बॉट को न पहचानने के लिए किन्नोपोइक के लिए, आपको अनुरोधों में हेडर तर्क को निर्दिष्ट करने की आवश्यकता है। फ़ंक्शन, जो ब्राउज़र को अनुकरण करेगा। उपयोगकर्ता-एजेंट, स्वीकार-भाषा और स्वीकार के साथ इसे शब्दकोश में पास करना आवश्यक है, जिसके मूल्यों को ब्राउज़र डेवलपर टूल में पाया जा सकता है। इसके बाद, एक पार्सर बनाया जाता है और समीक्षाएँ पृष्ठ से पुनर्प्राप्त की जाती हैं, जो _reachbanner_ html मार्कअप क्लास में संग्रहीत होती हैं।

import requests from bs4 import BeautifulSoup import numpy as np import time import os def load_data(url): r = requests.get(url, headers = headers) #  http  soup = BeautifulSoup(r.text, 'html.parser')#  html  reviews = soup.find_all(class_='_reachbanner_')#    reviews_clean = [] for review in reviews:#    html  reviews_clean.append(review.find_all(text=True)) return reviews_clean 

हमें एचटीएमएल मार्कअप से छुटकारा मिल गया, हालांकि, हमारी समीक्षा अभी भी ब्यूटीफुल ऑब्जेक्ट्स हैं, लेकिन हमें उन्हें स्ट्रिंग्स में बदलने की आवश्यकता है। कन्वर्ट फंक्शन बस यही करता है। हम एक फ़ंक्शन भी लिखेंगे जो फिल्म का नाम पुनर्प्राप्त करता है, जिसे बाद में समीक्षाओं को सहेजने के लिए उपयोग किया जाएगा।

 def convert(reviews): #     review_converted = [] for review in reviews: for i in review: map(str, i) review = ''.join(review) review_converted.append(review) return review_converted def get_name(url): #    r = requests.get(url, headers = headers) soup = BeautifulSoup(r.text, 'html.parser') name = soup.find(class_='alternativeHeadline') name_clean = name.find_all(text = True) #   , . .     return str(name_clean[0]) 

पार्सर का अंतिम कार्य मूवी के मुख्य पृष्ठ, एक समीक्षा वर्ग और समीक्षाओं को सहेजने का एक तरीका ले जाएगा। फ़ंक्शन उन अनुरोधों के बीच देरी को भी परिभाषित करता है जो प्रतिबंध से बचने के लिए आवश्यक हैं। फ़ंक्शन में एक लूप होता है जो पहले पृष्ठ से शुरू होने वाली समीक्षाओं को पुनर्प्राप्त करता है और संग्रहीत करता है, जब तक कि यह एक बिना पृष्ठ वाले पृष्ठ का सामना नहीं करता है जिसमें से load_data फ़ंक्शन एक खाली सूची निकालेगा और लूप टूट जाएगा।

 def parsing(url, status, path): page = 1 delays = [11, 12, 13, 11.5, 12.5, 13.5, 11.2, 12.3, 11.8] name = get_name(url) time.sleep(np.random.choice(delays)) #    while True: loaded_data = load_data(url + 'reviews/ord/date/status/{}/perpage/200/page/{}/'.format(status, page)) if loaded_data == []: break else: # E     ,    if not os.path.exists(path + r'\{}'.format(status)): os.makedirs(path + r'\{}'.format(status)) converted_data = convert(loaded_data) #   for i, review in enumerate(converted_data): with open(path + r'\{}\{}_{}_{}.txt'.format(status, name, page, i), 'w', encoding = 'utf-8') as output: output.write(review) page += 1 time.sleep(np.random.choice(delays)) 

फिर, निम्नलिखित चक्र का उपयोग करते हुए, आप उन फिल्मों से समीक्षा निकाल सकते हैं जो urles सूची में हैं। फिल्मों की सूची मैन्युअल रूप से बनानी होगी। उदाहरण के लिए, यह संभव होगा कि फिल्मों के लिंक की एक सूची लिखकर एक फंक्शन लिखी जाए, जो उन्हें किसी मूवी सर्च की शीर्ष 250 फिल्मों में से निकाले, इसलिए इसे मैन्युअल रूप से नहीं करना चाहिए, लेकिन 15-20 फिल्में प्रत्येक कक्षा के लिए एक हजार समीक्षाओं के छोटे डेटासेट बनाने के लिए पर्याप्त होंगी। इसके अलावा, यदि आपको कोई प्रतिबंध मिलता है, तो कार्यक्रम यह प्रदर्शित करेगा कि किस फिल्म और वर्ग ने प्रतिबंध को पारित करने के बाद उसी स्थान से जारी रखने के लिए रोका।

 path = #    urles = #    statuses = ['good', 'bad', 'neutral'] delays = [15, 20, 13, 18, 12.5, 13.5, 25, 12.3, 23] for url in urles: for status in statuses: try: parsing(url = url, status = status, path=path) print('one category done') time.sleep(np.random.choice(delays)) #       AttributeError except AttributeError: print(' : {}, {}'.format(url, status)) break #  else  ,      #    ,     else: print('one url done') continue break 

पूर्व उपचार


पार्सर लिखने के बाद, उनके लिए यादृच्छिक फिल्मों और फिल्म खोज के कई प्रतिबंधों को याद करते हुए, मैंने फ़ोल्डरों में समीक्षाओं को मिलाया और प्रशिक्षण के लिए प्रत्येक वर्ग से 900 और नियंत्रण समूह के लिए 900 समीक्षाओं का चयन किया। अब आवास को पूर्व-संसाधित करना आवश्यक है, अर्थात्, इसे टोकन और सामान्य करना। टोकनाइज़िंग का अर्थ है, पाठ को घटकों में तोड़ना, इस मामले में शब्दों में, क्योंकि हम शब्दों के एक बैग के प्रतिनिधित्व का उपयोग करेंगे। और सामान्यीकरण में शब्दों को निचले मामले में परिवर्तित करना, स्टॉप शब्द और अतिरिक्त शोर को दूर करना, स्टैमिंग और किसी भी अन्य चालें हैं जो संकेतों के स्थान को कम करने में मदद करती हैं।

हम आवश्यक पुस्तकालयों का आयात करते हैं।

छिपा हुआ पाठ
 from nltk.corpus import PlaintextCorpusReader from nltk.stem.snowball import SnowballStemmer from nltk.probability import FreqDist from nltk.tokenize import RegexpTokenizer from nltk import bigrams from nltk import pos_tag from collections import OrderedDict from sklearn.metrics import classification_report, accuracy_score from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import GridSearchCV from sklearn.utils import shuffle from multiprocessing import Pool import numpy as np from scipy.sparse import csr_matrix 


हम पाठ प्रीप्रोसेसिंग के लिए कुछ छोटे कार्यों को परिभाषित करके शुरू करते हैं। पहला, जिसे लोअर_पोस_टैग कहा जाता है , शब्दों के साथ एक सूची लेगा, उन्हें निचले मामले में बदल देगा, और प्रत्येक टोकन को अपने भाषण के भाग के साथ एक टुपल में बचाएगा। एक शब्द में भाषण के हिस्से को जोड़ने के संचालन को भाषण का हिस्सा (पीओएस) टैगिंग कहा जाता है और अक्सर एनएलपी में संस्थाओं को निकालने के लिए उपयोग किया जाता है। हमारे मामले में, हम शब्दों को फ़िल्टर करने के लिए भाषण के कुछ हिस्सों का उपयोग करेंगे।

 def lower_pos_tag(words): lower_words = [] for i in words: lower_words.append(i.lower()) pos_words = pos_tag(lower_words, lang='rus') return pos_words 

ग्रंथों में बड़ी संख्या में ऐसे शब्द होते हैं जो अक्सर मॉडल (तथाकथित स्टॉप शब्दों) के लिए उपयोगी होते हैं। मूल रूप से, ये प्रस्ताव, संयुग्मन, सर्वनाम हैं जिनके द्वारा यह निर्धारित करना असंभव है कि कौन से वर्ग को संदर्भित करता है। स्वच्छ फ़ंक्शन केवल संज्ञा, विशेषण, क्रिया और क्रिया विशेषण छोड़ देता है। ध्यान दें कि यह भाषण के कुछ हिस्सों को हटा देता है, क्योंकि उन्हें स्वयं मॉडल के लिए आवश्यक नहीं है। आप यह भी देख सकते हैं कि यह फ़ंक्शन स्टैमिंग का उपयोग करता है, जिसका सार शब्दों से प्रत्ययों और उपसर्गों को छोड़ना है। यह आपको संकेतों के आयाम को कम करने की अनुमति देता है, क्योंकि विभिन्न जेनरा और मामलों वाले शब्द एक ही टोकन में कम हो जाएंगे। स्टैमिंग - लेमेटेटाइजेशन का एक अधिक शक्तिशाली एनालॉग है, यह आपको शब्द के प्रारंभिक रूप को पुनर्स्थापित करने की अनुमति देता है। हालांकि, यह स्टैमिंग की तुलना में धीमी गति से काम करता है, और, इसके अलावा, एनएलटीके में रूसी लेमेटाइज़र नहीं है।

 def clean(words): stemmer = SnowballStemmer("russian") cleaned_words = [] for i in words: if i[1] in ['S', 'A', 'V', 'ADV']: cleaned_words.append(stemmer.stem(i[0])) return cleaned_words 

अगला, हम अंतिम फ़ंक्शन लिखते हैं जो क्लास लेबल ले जाएगा और इस वर्ग के साथ सभी समीक्षाओं को पुनः प्राप्त करेगा। मामले को पढ़ने के लिए, हम PlaintextCorpusReader ऑब्जेक्ट की कच्ची विधि का उपयोग करेंगे, जो आपको निर्दिष्ट फ़ाइल से पाठ निकालने की अनुमति देता है। अगला, टोकन का उपयोग RegexpTokenizer का उपयोग किया जाता है, जो एक नियमित अभिव्यक्ति के आधार पर काम करता है। अलग-अलग शब्दों के अलावा, मैंने मॉडल बिग्रेड्स में जोड़ा, जो सभी पड़ोसी शब्दों के संयोजन हैं। यह फ़ंक्शन FreqDist ऑब्जेक्ट का भी उपयोग करता है, जो शब्दों की घटना की आवृत्ति लौटाता है। इसका उपयोग यहां उन शब्दों को हटाने के लिए किया जाता है जो किसी विशेष वर्ग की सभी समीक्षाओं में केवल एक बार दिखाई देते हैं (उन्हें हापाक भी कहा जाता है)। इस प्रकार, समारोह में एक शब्दकोष होगा जिसमें शब्दों के एक बैग के रूप में प्रस्तुत किए गए दस्तावेज़ और एक विशेष वर्ग के लिए सभी शब्दों की सूची होगी।

 corpus_root = #    def process(label): # Wordmatrix -     # All words -    data = {'Word_matrix': [], 'All_words': []} #      templist_allwords = [] #        corpus = PlaintextCorpusReader(corpus_root + '\\' + label, '.*', encoding='utf-8') #       names = corpus.fileids() #   tokenizer = RegexpTokenizer(r'\w+|[^\w\s]+') for i in range(len(names)): #   bag_words = tokenizer.tokenize(corpus.raw(names[i])) lower_words = lower_pos_tag(bag_words) cleaned_words = clean(lower_words) finalist = list(bigrams(cleaned_words)) + cleaned_words data['Word_matrix'].append(final_words) templist_allwords.extend(cleaned_words) #   templistfreq = FreqDist(templist_allwords) hapaxes = templistfreq.hapaxes() #    for word in templist_allwords: if word not in hapaxes: data['All_words'].append(word) return {label: data} 

पूर्व-प्रसंस्करण चरण सबसे लंबा है, इसलिए यह हमारे मामले के प्रसंस्करण को समानांतर बनाने के लिए समझ में आता है। यह मल्टीप्रोसेसिंग मॉड्यूल का उपयोग करके किया जा सकता है। प्रोग्राम कोड के अगले टुकड़े में, मैं तीन प्रक्रियाएं शुरू करता हूं जो एक साथ तीन फ़ोल्डरों को अलग-अलग वर्गों के साथ संसाधित करेगा। इसके बाद, परिणाम एक शब्दकोश में एकत्र किए जाएंगे। यह प्रीप्रोसेसिंग पूरा हो गया है।

 if __name__ == '__main__': data = {} labels = ['neutral', 'bad', 'good'] p = Pool(3) result = p.map(process, labels) for i in result: data.update(i) p.close() 

vectorization


मामले को पूर्व-संसाधित करने के बाद, हमारे पास एक शब्दकोश है जहां प्रत्येक वर्ग लेबल में समीक्षाओं के साथ एक सूची है जिसमें हम टोकन के साथ सामान्यीकृत और समृद्ध हैं, साथ ही साथ इस वर्ग के सभी समीक्षाओं के शब्दों की एक सूची है। चूंकि मॉडल प्राकृतिक भाषा का अनुभव नहीं कर सकता जैसा कि हम करते हैं, अब कार्य हमारी समीक्षाओं को संख्यात्मक रूप में प्रस्तुत करना है। ऐसा करने के लिए, हम एक सामान्य शब्दावली बनाएंगे, जिसमें अद्वितीय टोकन होंगे, और इसके साथ हम प्रत्येक समीक्षा को वेक्टर करेंगे।

शुरू करने के लिए, हम एक सूची बनाते हैं जिसमें उनके लेबल के साथ सभी वर्गों की समीक्षाएं होती हैं। इसके बाद, हम एक सामान्य शब्दावली बनाते हैं, जो एक ही FreqDist की सबसे असामान्य विधि का उपयोग करते हुए सबसे सामान्य शब्दों के प्रत्येक वर्ग 10,000 से लेते हैं । नतीजतन, मुझे एक शब्दावली मिली जिसमें लगभग 17,000 शब्द थे।

 #     : # [([  ], _)] labels = ['neutral', 'bad', 'good'] labeled_data = [] for label in labels: for document in data[label]['Word_matrix']: labeled_data.append((document, label)) #      all_words = [] for label in labels: frequency = FreqDist(data[label]['All_words'] common_words = frequency.most_common(10000) words = [i[0] for i in common_words] all_words.extend(words) #    unique_words = list(OrderedDict.fromkeys(all_words)) 

पाठ को सदिश करने के कई तरीके हैं। उनमें से सबसे लोकप्रिय: TF-IDF, प्रत्यक्ष और आवृत्ति कोडिंग। मैंने आवृत्ति कोडिंग का उपयोग किया, जिसका सार प्रत्येक समीक्षा को वेक्टर के रूप में प्रस्तुत करना है, जिनमें से तत्व शब्दावली से प्रत्येक शब्द की घटनाओं की संख्या है। एनएलटीके के अपने स्वयं के क्लासिफायर हैं, आप उनका उपयोग कर सकते हैं, लेकिन वे अपने समकक्षों की तुलना में धीमी -सीख से काम करते हैं और कम सेटिंग्स रखते हैं। नीचे NLTK के लिए कोडिंग का कोड दिया गया है । हालाँकि, मैं नाइकी बेयस मॉडल का उपयोग स्किकिट-लर्न से करूंगा और समीक्षाओं को एनकोड करूंगा, जो कि साइपी से एक विरल मैट्रिक्स में विशेषताओं को संग्रहीत करता है, और एक अलग न्यूपी सरणी में क्लास लेबल।

 #     nltk  : # # [({ : -   },  )] prepared_data = [] for x in labeled_data: d = defaultdict(int) for word in unique_words: if word in x[0]: d[word] += 1 if word not in x[0]: d[word] = 0 prepared_data.append((d, x[1])) #     scikit-learn #     matrix_vec = csr_matrix((len(labeled_data), len(unique_words)), dtype=np.int8).toarray() #     target = np.zeros(len(labeled_data), 'str') for index_doc, document in enumerate(labeled_data): for index_word, word in enumerate(unique_words): #  -     matrix_vec[index_doc, index_word] = document[0].count(word) target[index_doc] = document[1] #   X, Y = shuffle(matrix_vec, target) 

चूंकि डेटासेट में कुछ टैग्स के साथ समीक्षाएँ एक के बाद एक जाती हैं, अर्थात्, पहले सभी तटस्थ, फिर सभी नकारात्मक और इसी तरह, आपको उन्हें मिश्रण करने की आवश्यकता है। ऐसा करने के लिए, आप scikit-learn से फेरबदल फ़ंक्शन का उपयोग कर सकते हैं । यह केवल स्थितियों के लिए उपयुक्त है जब संकेत और वर्ग लेबल विभिन्न सरणियों में होते हैं, क्योंकि यह आपको दो सरणियों को एक साथ मिलाने की अनुमति देता है।

मॉडल प्रशिक्षण


अब यह मॉडल को प्रशिक्षित करने और नियंत्रण समूह में इसकी सटीकता की जांच करने के लिए बना हुआ है। एक मॉडल के रूप में, हम Naive Bayes क्लासिफायर के मॉडल का उपयोग करेंगे। Scikit-learn में डेटा के वितरण के आधार पर तीन Naive Bayes मॉडल हैं: बाइनरी, असतत और निरंतर। चूंकि हमारी सुविधाओं का वितरण असतत है, इसलिए हम मल्टीनोमियलएनबी का चयन करते हैं।

बायेसियन क्लासिफायर में अल्फा हाइपर पैरामीटर है , जो मॉडल को चौरसाई करने के लिए जिम्मेदार है। Naive Bayes सभी वर्गों से संबंधित प्रत्येक समीक्षा की संभावनाओं की गणना करता है, इसके लिए सभी समीक्षा शब्दों की उपस्थिति की सशर्त संभावनाओं को गुणा करते हैं, बशर्ते कि वे किसी विशेष वर्ग के हों। लेकिन अगर प्रशिक्षण डेटा सेट में कुछ समीक्षा शब्द नहीं मिला, तो इसकी सशर्त संभावना शून्य के बराबर है, जो इस संभावना को शून्य करता है कि समीक्षा किसी भी वर्ग की है। इससे बचने के लिए, डिफ़ॉल्ट रूप से, सभी सशर्त शब्द संभावनाओं में एक इकाई को जोड़ा जाता है, अर्थात अल्फा एक के बराबर होता है। हालाँकि, यह मान इष्टतम नहीं हो सकता है। आप ग्रिड खोज और क्रॉस सत्यापन का उपयोग करके अल्फा का चयन करने का प्रयास कर सकते हैं।

 parameter = [1, 0, 0.1, 0.01, 0.001, 0.0001] param_grid = {'alpha': parameter} grid_search = GridSearchCV(MultinomialNB(), param_grid, cv=5) grid_search.fit(X, Y) Alpha, best_score = grid_search.best_params_, grid_search.best_score_ 

मेरे मामले में, ग्रिड चूल्हा 0.965 की सटीकता के साथ 0 के बराबर हाइपरपरमीटर का इष्टतम मूल्य देता है। हालांकि, यह मान स्पष्ट रूप से नियंत्रण डेटासेट के लिए इष्टतम नहीं होगा, क्योंकि बड़ी संख्या में ऐसे शब्द होंगे जो प्रशिक्षण सेट में पहले नहीं पाए गए हैं। संदर्भ डेटासेट के लिए, इस मॉडल की सटीकता 0.598 है। हालांकि, यदि आप अल्फा को 0.1 तक बढ़ाते हैं, तो प्रशिक्षण डेटा पर सटीकता 0.82 तक गिर जाएगी, और नियंत्रण डेटा पर यह 0.62 तक बढ़ जाएगा। सबसे अधिक संभावना है, एक बड़े डेटा सेट पर, अंतर अधिक महत्वपूर्ण होगा।

 model = MultinomialNB(0.1) model.fit(X, Y) # X_control, Y_control   ,   X  Y #        predicted = model.predict(X_control) #     score_test = accuracy_score(Y_control, predicted) #   report = classification_report(Y_control, predicted) 


निष्कर्ष


यह माना जाता है कि मॉडल का इस्तेमाल उन समीक्षाओं की भविष्यवाणी करने के लिए किया जाना चाहिए जिनके शब्दों का इस्तेमाल शब्दावली बनाने के लिए नहीं किया गया था। इसलिए, मॉडल की गुणवत्ता का मूल्यांकन डेटा के नियंत्रण भाग पर इसकी सटीकता से किया जा सकता है, जो 0.62 है। यह सिर्फ अनुमान लगाने से लगभग दोगुना बेहतर है, लेकिन सटीकता अभी भी बहुत कम है।

वर्गीकरण रिपोर्ट के अनुसार, यह स्पष्ट है कि मॉडल एक तटस्थ रंग (सटीकता 0.47 बनाम 0.68 सकारात्मक के लिए और 0.76 नकारात्मक के लिए) के साथ सबसे खराब प्रदर्शन करता है। दरअसल, तटस्थ समीक्षाओं में ऐसे शब्द होते हैं जो सकारात्मक और नकारात्मक दोनों समीक्षाओं की विशेषता होते हैं। संभवतः, डेटासेट की मात्रा में वृद्धि करके मॉडल की सटीकता में सुधार किया जा सकता है, क्योंकि तीन-हजारवां डेटा सेट बल्कि मामूली है। साथ ही, समस्या को सकारात्मक और नकारात्मक में द्विआधारी वर्गीकरण की समस्या को कम करना संभव होगा, जिससे सटीकता भी बढ़ेगी।

पढ़ने के लिए धन्यवाद।

PS यदि आप स्वयं अभ्यास करना चाहते हैं, तो मेरे डेटासेट को लिंक से नीचे डाउनलोड किया जा सकता है।

डेटासेट से लिंक करें

Source: https://habr.com/ru/post/hi467081/


All Articles