Този блог е първият от поредицата за анализ на настроението върху данните от Zomato. Има много популярни модели като SVM, Random Forest и Naive Bayes и т.н., които да се използват в Sentiment Analysis. В тази серия ще използваме модела LSTM заедно с Feedforward, за да предвидим настроението на рецензията. Преди да преминем към архитектурата на модела, трябва да изпълним някои задачи за предварителна обработка на данните, така че да стане приемливо за нашия модел да научи функции и да класифицира.

Относно данните

Данните, които ще използваме, са от Zomato и са извлечени от Himanshu Poddar.

Зареждаме набора от данни в рамката с данни на pandas. Човек може да инспектира набора от данни и да извърши основен проучвателен анализ на данни, за да изгради известна интуиция върху данните.

Трябва ни само колоната reivews_list от рамката с данни. Разглеждаме данните в колоната с списък с отзиви и можем да видим, че всички отзиви, написани за всеки ресторант, присъстват заедно като една стойност в колоната списък с отзиви.

print('Restaurant Name: 'df['name'][4])
df['reviews_list'][4]

При анализа на данните виждаме, че това не са най-чистите данни, които можем да имаме и трябва да ги подготвим и почистим. Следователно, ние изрязахме нашата задача за подготовка и предварителна обработка на данни като:

  • Разделете оценките и отзивите в различни колони или списъци
  • Проверете и почистете данните за оценки
  • Премахнете шума, свързан с текста, от рецензиите
  • Нормализиране на текст
  • Произход / Лематизиране
  • Премахване на думи с ниска честота в корпуса

Разделете оценките и рецензиите

  • Разделете на двойка рецензия-оценка:Всички рецензии заедно с техните оценки присъстват заедно. Затова първо ги разделяме и третираме всяка рецензия с нейната оценка като отделна стойност. За тази цел намираме последователността от низове, която разделя всяка двойка преглед-оценка и я използваме за разделяне.

split_reviews=reviews.apply(lambda x: x[9:].split(', (\'Rated '))

Тук отзивие серията пандисъдържаща колона списък_отзиви от рамката с данни на zomato. Резултатът от операцията е показан по-долу.

  • Премахване на дублиращи се рецензии:Виждаме множество дублирани рецензии при анализа, така че ги премахваме.
split_reviews=split_reviews.apply(lambda x: list(set(x)))

[ Забележка:Функцията set() променя подреждането на списъка, но тъй като двойката преглед-оценка присъства заедно, няма да имаме проблеми с подреждането.]

  • Разделяне на двойката преглед-оценка:Сега отделяме оценката и прегледа като отделни стойности, като правим отделни списъци за тях, но запазваме реда.
def extract_ratings(x):
  ratings=[]
  for i in x:
    ratings.append(i[:1])
  return ratings
def extract_reviews(x):
  reviews=[]
  for i in x:
    reviews.append(i[14:-2])
  return reviews
#Split reviews and ratings
all_ratings=split_reviews.apply(lambda x: extract_ratings(x))
all_reviews=split_reviews.apply(lambda x: extract_reviews(x))

След операцията получаваме отзиви и оценки като отделен списък, както е показано:

Сега обединяваме оценките и рецензиите на всички ресторанти в едно измерение.

rating_list=[rating for restaurant_ratings in all_ratings for rating in restaurant_ratings]
review_list=[review for restaurant_reviews in all_reviews for review in restaurant_reviews]

Списък с рейтинги за почистване и обработка:

От получената двойка 558778 рецензия-оценка можем да предположим известно несъответствие в процеса на извличане. Трябва да гарантираме, че цялата стойност на рейтинга е числова стойност и можем също така да преобразуваме типа данни в int за удобство. Така че изпълняваме следните стъпки:

  • Проверка и премахване на нечислови рейтингови стойности:Виждаме от 558778 оценки, 7595 не са числови. Това може да се дължи на някаква ненужна стойност или неточно извличане. Така че ние премахваме тези случаи на оценка заедно със съответните им рецензии.
#storing index of non-numeric rating instances
non_numeric_rating=[i for i, j in enumerate(rating_list) if not j.isnumeric()]
print("Number of non numeric rating instances",len(non_numeric_rating))
#Outputs 7595
#pop-ing in reverse order of the index
for i in non_numeric_rating[::-1]:
  print(i)
  rating_list.pop(i)
  review_list.pop(i)

[Забележка: За премахване на нечисловите екземпляри от оценките ние изскачаме (премахваме) екземплярите в обратна посока, така че корекцията на индекса, възникваща след премахване на елемент в списъка, е извън диапазона от индекси, извикани в следващите итерации]

  • Преобразуване на стойността на рейтинга от низ в тип данни int:Стойността на числовия низ се преобразува в цяло число.
rating_list=list(map(int,rating_list))

Премахнете текстовия шум от рецензиите

След това премахваме текстов шум като пунктуация, празни интервалиs и не-ASCII знациот нашия текст, което добавя малка или никаква стойност за задачата за анализ на настроението.

#Remove punctuation
def remove_punc(contents):
  punc_list=(string.punctuation).replace
  table=contents.maketrans(string.punctuation,' ' *
  len(string.punctuation))
  return contents.translate(table)
#Remove white spaces
def remove_white_space(data):
  return ' '.join(data.split())
#Remove word containing non ascii characters
def remove_non_ascii_words(contents):
  string_ascii = ' '.join([ token for token in contents.split() if token.isascii() ])
  return string_ascii
#Noise Removal
def remove_noise(contents):
  remove_pun=remove_punc(contents)
  remove_spaces=remove_white_space(remove_pun)
  remove_non_ascii=remove_non_ascii_words(remove_spaces)
  return remove_non_ascii
review_no_noise=[remove_noise(i) for i in review_list]

По време на различните стъпки на предварителната обработка са използвани различни променливи (remove_pun, remove_spaces и т.н.), както е показано по-горе, вместо презаписване върху същите. Това се прави за достъп и проучване на данни при различните стъпки на предварителна обработка. Ако няма изискване като такова, може да се използва същата променлива, тъй като ще заема по-малко място в паметта.

Нормализиране на текста

След като премахнем шумовете, сега нормализираме текста, като изпълним следните стъпки:

  • Преобразуване на текст в малки букви:Това гарантира, че думите с еднакви букви, но с различни регистри се третират като една и съща дума.
  • Токенизиране на текста:Разбийте текстовите токени на думи. Това е фундаментална стъпка в обработката на естествен език, тъй като помага на модела да изведе значението на текста чрез присъстващите токени/думи.
  • Премахване на спирачките:Те са прекалено използвани думи в текста, което добавя много малка стойност към текста и следователно може да бъде игнорирано. Някои спирателни думи на английски могат да бъдат полезни при анализ на настроенията, като надолу, защо, по-долу и т.н.Така че ги включваме.
#text to lower case
def to_lower(contents):
  return contents.lower()
#tokenize words
nltk.download('punkt')
def tokenize(contents):
  return nltk.word_tokenize(contents)
#remove_stopwords
nltk.download('stopwords')
def remove_stopwords(contents):
  cachedStopWords = stopwords.words("english")[:121]
  include_words=['until','above','below','down','up','under','over','out','why','how','all','any','both','each']
  for i in include_words:
    cachedStopWords.remove(i)
  no_stopwords=[]
  for i in tqdm(contents,desc='Stopword Removal'):
    no_stopwords.append([j for j in i if j not in cachedStopWords])
  return no_stopwords

tokens=[]
for i in review_no_noise:
  review_tolower=to_lower(i)
  tokens.append(nltk.word_tokenize(review_tolower))
  no_stopwords=remove_stopwords(tokens)
tokens

Стимулиране и лематизация

В NLP, Stemming и Lemmatizationредуцират думата до нейната основна дума. Въпреки че и двамата вършат подобен вид работа, има някои разлики. Стимулиранесъкращава думата до нейната основна дума. Премахва последните няколко знака от думата. При лематизацията,контекстът на думата се разглежда и след това се минимизира до нейната коренна дума, известна още като лема.

Лематизациятае по-скъп изчислителен процес и отнема много време от Стеминг. За големи набори от данни Stemming става единствената опция от двете. Нещо повече, лематизацията изисква таг за части от речта върху думите за създаване на правилната лема. Въпреки това, лематизацията понякога произвежда по-точни основни думи от Stemming.

Тук ще използваме Lemmatization, в нашата предварителна обработка. Следвайки стъпките:

  • POS маркиране:За да бъдат нашите леми точни, частите на речта на всяка дума трябва да се знаят (въпреки че за удобство можете да обобщите всички думи като глагол за лематизация). Маркираме всички думи с техните POS, като използваме nltk.pos_tag().
pos_tag=[]
for i in tqdm(no_stopwords,desc='POS Tagging'):
  pos_tag.append(nltk.pos_tag(i))

WordNetLemmatizer(), използван за лематизиране, разпознава само четири части на речта: прилагателно, глагол, съществително и наречие. Така че трябва да преобразуваме всички класове (които са предимно подкласове на тези четири POS) в един от тези четири класа.

def pos_tagger(nltk_tag):
  if nltk_tag.startswith('J'):
    return wordnet.ADJ
  elif nltk_tag.startswith('V'):
    return wordnet.VERB
  elif nltk_tag.startswith('N'):
    return wordnet.NOUN
  elif nltk_tag.startswith('R'):
    return wordnet.ADV
wordnet_tagged=[]
for i in tqdm(pos_tag,desc='Tag conversion'):
  wordnet_tagged.append(list(map(lambda x: (x[0], pos_tagger(x[1])), i)))
  • Лематизирайте POS маркираните думи:Ние използваме WordNetLemmatizer(), за да лематизираме POS маркираните маркери(думи) за преглед.
def lemmatization(contents):
  lem = WordNetLemmatizer()
  lem_list=[]
  for i in tqdm(contents,desc='Lemmatization'):
    lem_review=[]
    for j in i:
      if j[1] is None:
        lem_review.append(j[0])
      else:
        lem_review.append(lem.lemmatize(j[0],j[1]))
    lem_list.append(lem_review)
  return lem_list
lemmatized_review=lemmatization(wordnet_tagged)

Премахнете нискочестотните думи

Накрая, след цялата стандартна предварителна обработка, трябва да изпълним някои специфични за набора от данни задачи за предварителна обработка. Това включва главно премахване на редки думив набора от данни. Защо? Това е така, защото ако една дума е изключително рядка, тогава нашият модел ще има много по-малко възможности да научи или да извлече нещо от нея. В случай, че моделът научава такова рядко явление, това може да доведе до прекомерно напасванена модела.

Следват стъпките за премахване на редки думи:

  • Намиране на речник на думите, присъстващи в корпуса
  • Намерете честотата на всяка дума в корпуса
  • Премахнете редки думи въз основа на тяхната честота

Намиране на речник на думите, присъстващи в корпуса:Първо намираме всички думи, използвани в корпуса с функцията gensim.corpora.Dictionary от библиотеката gensim.

import gensim
from gensim import corpora
from gensim.corpora import Dictionary
review_dct=Dictionary(lemmatized_review)

Намерете честотата на всяка дума в корпуса:Използвайки думите от речника, намираме съответната честота, свързана с думата в корпуса. За тази цел ние създаваме торба с думи за всеки преглед, като използваме вече създадения речник на думите. Използваме doc2bow() за тази задача. След това натрупваме честотите на думите от всички рецензии, използвайки функцията брояч в модула за събиране в python.

corpus = [review_dct.doc2bow(sent) for sent in tqdm(lemmatized_review,desc='Term Frequency')]
vocab_tf_row=[dict(i) for i in corpus]
counter=collections.Counter()
for i in tqdm(vocab_tf_row,desc='Accumulating Frequencies'):
  counter.update(i)
#converting counter object to dictionary object 
res=dict(counter)

Премахване на редки думи от корпуса:Сега, след като имаме честота на всяка дума в речника в целия корпус, можем да зададем праговата честота, под която дадена дума може да се счита за рядка, а останалата част може да се използва за нашите работа. Можем да започнем с 5 като прагова честота.

#Creating list of rare words and index having frequency less than 5 in the corpus
#Creating list of rare words and index having frequency more than or equal to 5 in the corpus
working_words_ids=[]
rare_words_ids=[]
for i,j in tqdm(res.items(),desc='Counting low Frequent indices'):
  if j<5:
    rare_words_ids.append(i)
  else:
    working_words_ids.append(i)
working_words=[review_dct[i] for i in working_words_ids]
rare_words=[review_dct[i] for i in rare_words_ids]

Можем да видим, че тези редки думи са предимно безсмислени или са неправилно написани думи.

Сега може да има някои рецензии, които съдържат само редки думи. Трябва да ги премахнем със съответната им оценка от списъците. След това премахваме редките думи от корпуса.

#Creating a review corpus with working words having no rare words
working_corpus=[]
difference_list=[]
for i in tqdm(lemmatized_review):
  working_corpus.append([j for j in i if j in working_words])

И… Ето! Сега имаме нашия набор от данни във формата, която може да бъде подаден към модела за прогнозиране.

Можете да следвате връзката, за да намерите бележника, съдържащ кодовете, споменати в този блог. Използваните данни принадлежат на Zomato Ltd и са извлечени от Himanshu Poddar. Моля, дайте необходимите кредити, ако използвате данните.