Этот блог является второй частью серии Анализ настроений по данным Zomato. Если вы не следили за первым блогом, мы выполнили ряд задач предварительной обработки в столбце обзоров набора данных Zomato, чтобы преобразовать необработанные обзоры и их рейтинги в подходящую форму для нашей модели, с которой можно работать, и дать хорошие результаты. Нажмите здесь, если хотите прочитать блог по предобработке данных.

В этом блоге мы разработаем нашу модель прогнозирования настроений и постараемся подробно обсудить архитектуру. Мы будем использовать долговременную кратковременную память или LSTM (двунаправленную) вместе с Feedforward Networkв нашей модели для изучения и прогнозирования тональности отзывов Zomato.

Прежде чем мы начнем разрабатывать нашу модель, давайте прольем свет на концепцию модели последовательности RNN и LSTM.

Модели последовательности

Хотя ряд примитивных моделей, таких как SVM, наивный байесовский метод и случайный лес, похоже, хорошо справляются с задачей анализа тональности, использование методов глубокого обучения, таких как RNN, дает более динамичные и значимые результаты. функции, чем методы извлечения функций на основе подсчета (например, TF-IDF), используемые в примитивных моделях, где теряется много информации. В контексте текста RNN довольно хорошо хранит информацию о порядке слов и близости.

В моделях последовательности, таких как рекуррентная нейронная сеть, мы используем информацию из предыдущего временного шага вместе с текущими входными данными для вычисления выходных данных нашего текущего временного шага. Алгоритм обучения представляет собой градиентный спуск. Однако эффективность RNN снижается для более длинных последовательностей. В более длинной последовательности градиент, по-видимому, плохо течет к удаленным единицам из-за многократного умножения положительной величины меньше 1. Чем больше количество шагов, тем больше будут эти умножающие элементы, тем меньше будет умноженный результат (который будет близок до 0).

Умножение 0,9, 100 раз = 0,9 * 0,9 *… * 0,9 = 0,9¹⁰⁰=0,00002656139

Следовательно, потеря вряд ли поможет в изучении дальней единицы (например, s-1 из s-101, 100 шагов).

Чтобы решить проблему исчезающего градиента RNN, мы приходим к LSTM. LSTM использует три вентиля: забыть вентиль, входной вентиль и выходной вентиль, который управляет прямым распространением (сколько информации должно быть взято из предыдущего, а также текущего временного шага), а также обратным распространением информацию о градиенте к разным временным шагам. Он имеет блок памяти, который помогает запоминать значения состояния за различные промежутки времени. Уникальный дизайн аддитивного градиента помогает градиенту течь к удаленным устройствам.

Подведение итогов

Мы разделим нашу задачу анализа настроений на следующие этапы:

  • Сегментировать рейтинги по категориям
  • Балансировка набора данных по категориям
  • Разделить данные на обучение, проверку и тестирование
  • Класс модели дизайна
  • Реализовать пакетную обработку
  • Обучение и оценка
  • Настройка гиперпараметров

Сегментация рейтинга по категориям

Когда мы загружаем наши предварительно обработанные данные обзоров и списка оценок, мы хотим проверить диапазон значений оценок в наших данных.

Значения рейтинга представляют собой натуральные числа в диапазоне от 1 до 5. Для нашей модели прогнозирования настроений мы хотим ограничить настроения тремя категориями: положительными, нейтральными и отрицательными. Для этого нам нужно сегментировать значения рейтинга для каждой категории настроений. Мы находим среднее значение оценок равным 3,52. Таким образом, мы устанавливаем значение рейтинга 3 для нейтральной категории, выше 3 мы считаем положительным, а ниже 3 - отрицательным.

#Sentiments: 0->Negative, 1->Neutral, 2->Positive
sentiment=[]
for i in ratings:
  if(i<3):
    sentiment.append(0)
  elif (i==3):
    sentiment.append(1)
  else :
    sentiment.append(2)
#Storing reviews and sentiment in X, Y
X=corpus.copy()
Y=sentiment.copy()

Мы отмечаем положительное настроение как 2, нейтральное как 1 и отрицательное как 0.

Балансировка набора данных

Проверяя подсчет каждого настроения в данных, мы обнаруживаем, что положительных отзывов о настроениях довольно много по сравнению с нейтральными и отрицательными. Это станет проблемой при обучении, поскольку наша модель станет более предвзятой к классу позитивных настроений, поскольку модель больше обучается на положительных отзывах о классе.

Нам нужно удалить из данных лишние записи положительных настроений, чтобы сделать распределение классов похожим. Мы удаляем 50 000 записей положительных настроений.

count=0
target_removals=50000
for i in range(len(Y)-1,-1,-1):
  if Y[i]==2:
    X.pop(i)
    Y.pop(i)
    count+=1
  if count==target_removals:
    break

Разделение данных обучения, проверки и тестирования

Мы разделили наши данные на наборы для обучения, проверки и тестирования. Мы обучаем нашу модель на данных поезда и проверяем точность прогноза с помощью набора проверки. В соответствии с точностью, достигнутой на данных проверки, мы корректируем параметры и переобучаемся на данных поезда. Когда мы достигнем удовлетворительной точности проверки, мы выясним, как наша модель работает с нетронутыми тестовыми данными.

Мы сохраняем соотношение поезд-валидация-тест в формате 70:15:15.

X_train,X_testing,Y_train,Y_testing=train_test_split(X,Y,test_size=0.3,random_state=0,stratify=Y)
X_val,X_test,Y_val,Y_test=train_test_split(X_testing,Y_testing,test_size=0.5,random_state=0,stratify=Y_testing)

Дизайн класса модели

Мы столкнулись с тем, как LSTM решают проблему исчезающего градиента и лучше подходят для более длинных задач последовательности. Однако из-за высокой сложности период обучения для LSTM высок. Чтобы решить эту проблему, часто используются Gated Recurrent Units (GRU), которые менее сложны и имеют меньше параметров для изучения, чем LSTM. Это приводит к более быстрому обучению. Однако это не гарантирует эквивалентную или более высокую точность, чем LSTM.

Двунаправленные единицы последовательности.В таких задачах, как прогнозирование тональности обзора, у нас есть полная последовательность, прежде чем мы вычислим выходные данные для последнего временного шага последовательности. Итак, мы можем запустить нашу модель последовательности с каждого конца и добавить выходные данные модели последовательности с обоих направлений, чтобы сформировать выходной слой модели. Это делается с помощью Bi-LSTM и Bi-GRU. Здесь два набора LSTM/GRU работают в противоположном направлении. Уровень активации берет значение из обоих наборов LSTM/GRU.

С точки зрения повышения точности, мы рано достигли крыши в LSTM по сравнению с BiLSTM. Итак, мы создаем класс BiLSTM. Мы обсудим некоторые важные переменные класса в этом классе и их функцию.

скрытый размер, выходной размер:указывает размер скрытого слоя. В LSTM значение скрытого слоя передается на следующий шаг последовательности. Размер вывода зависит от количества классов, доступных для нашего прогноза.

количество слоев: мы также можем решить, сколько последовательных слоев LSTM будет иметь наша сеть. Наличие нескольких слоев увеличивает сложность, параметры и вычисления сетевой функции, но также захватывает более сложные функции. Это компромиссная ситуация, которую нам нужно выяснить.

встраивание: он преобразует данные более высокого измерения в более низкое измерение. Нам нужно указать размер встраивания. В нашей задаче каждое слово в корпусном словаре представляет собой вектор с одним горячим номером. Эти векторы очень многомерны, в зависимости от количества слов в словаре (обычно десятки тысяч). Мы используем встраивание для преобразования таких многомерных данных в более низкое измерение (обычно в 100 с). При обучении нашей модели наша модель пытается выучить правильные вложения этих слов, чтобы похожие слова были ближе друг к другу.

Чтобы сэкономить время и ресурсы для изучения этих вложений, можно быстро перейти к предварительно обученным вложениям. Популярные предварительно обученные встраивания слов, такие как glove, доступны бесплатно.

отсев: отсев — это метод регуляризации, при котором часть сетевых единиц обнуляется или игнорируется. Это предотвращает сложную совместную адаптацию обучающих данных. Это метод усреднения модели, который делает нашу сеть надежной, предотвращая ее переоснащение.

Единица lstm: LSTM принимает вложения слов в качестве входных данных и применяет функцию преобразования, чтобы получить на выходе размер скрытой единицы. Может быть несколько последовательных блоков LSTM, которые, в свою очередь, могут быть однонаправленными или двунаправленными. Также мы можем использовать отсев на модулях LSTM.

скрытый для вывода модуль прямой связи: мы используем это на последнем временном шаге последовательности, где выходные данные LSTM принимаются в качестве входных данных, а функция преобразования типичной сети прямой связи применяется для получения выходных данных размер задается переменной выходного размера.

class BiLSTM_net(nn.Module):
  def __init__(self,vocab_size, hidden_size,num_layers,embedding_dim, output_size,dropout):
    super(BiLSTM_net,self).__init__()
    self.hidden=hidden_size
    self.num_layers=num_layers
    self.embedding=nn.Embedding(vocab_size,embedding_dim)
    self.lstm_cell = nn.LSTM(embedding_dim,hidden_size,num_layers=num_layers,dropout=0.3,bidirectional=True,batch_first=True)
    self.dropout=nn.Dropout(dropout)
    self.h2o=nn.Linear(hidden_size*2,output_size)
    self.softmax=nn.LogSoftmax(dim=2)
  def forward(self,input_,hidden_=None,batch_size=1,rev_len=None,device='cpu'):
    emb=self.embedding(input_.to(torch.long))
    out,hidden=self.lstm_cell(emb,hidden_)
    hidden=self.dropout(torch.cat((hidden[0][-2:-1,:,:],hidden[0] [-1:,:,:]),dim=2))
    output=self.h2o(hidden)
    output=self.softmax(output)
    return output.view(-1,3),hidden

В функции переадресации мы делаем следующие шаги:

я. Возьмите вложения слов из слов последовательности в качестве входных данных для LSTM и получите значение скрытых единиц для всех временных шагов. Если используются двунаправленные LSTM, для каждого временного шага в одном слое LSTM будут генерироваться два набора значений скрытых единиц.

II. Объедините двунаправленные выходы.

III. Примените отсев на скрытых выходах, тем самым обнулив часть выходов.

IV. Передайте остальную часть вывода на уровень прямой связи, который при применении своей функции преобразования выдает вывод для каждого класса/категории в нашем прогнозе.

v. Примените softmax к выходным данным, чтобы получить распределение вероятностей для выходных данных класса.

Дозирование

В отличие от данных, не относящихся к последовательности, обучение нашей модели в пакетах затруднено в модели последовательности. Это связано с различной длиной входных данных последовательности. Таким образом, возникают трудности с предоставлением размера входных данных для нашей прямой функции.

Для них мы используем padding,где мы берем набор входных последовательностей, находим длину (max_len) входных данных с самой длинной последовательностью. Мы делаем длину всей входной последовательности в пакете постоянной с этой максимальной длиной (max_len). Для последовательностей, которые меньше max_len, мы заполняем пустые места такими значениями, как 0. Таким образом, у нас есть входной размер для всей партии без потери какой-либо информации из ввода.

def batched_review_rep(reviews,max_len):
  batch_size=len(reviews)
  len_word_vec=len(words)
  rep=torch.zeros(batch_size,max_len)
  for rev_index,review in enumerate(reviews):
    diff_len=max_len-len(review)
    for word_seq, word in enumerate(review):
      rep[rev_index][word_seq+diff_len]=words.index(word)
  return rep.to(torch.long)

def batched_dataloader(n_points,X_,Y_,verbose=False,device='cpu'):
  len_X_=len(X_)
  reviews=[]
  ratings=[]
  reviews_len=[]
  for i in range(n_points):
    index=np.random.randint(len_X_)
    review,rating=X_[index],Y_[index]
    reviews_len.append(len(review))
    reviews.append(review)
    ratings.append(rating)
  max_len=max(reviews_len)
  reviews_rep=batched_review_rep(reviews,max_len).to(device)
  ratings_rep=torch.tensor(ratings).to(device)
  return reviews,ratings,reviews_rep,ratings_rep,torch.tensor(reviews_len)

Настройка обучения и оценки

Нам нужно определить нашу функцию обучения для обучения нашей модели. После обучения нам также необходимо оценить обученную модель на проверочных или тестовых данных с помощью функции оценки.

функция обучения: мы будем тренироваться партиями. Для этого нам нужно указать нашей функции обучения размер пакета, а также количество пакетов нам нужно обучить. Мы также указываем оптимизаторы моделей для обратного распространения ошибки и корректировки веса.

Мы начинаем обучение каждой партии, переводя сетевую модель в режим обучения и сбрасывая значения градиента оптимизаторов модели на ноль. Из функции пакетной обработки мы группировали данные указанного размера, а затем использовали функцию пересылки нашей модели, чтобы получить прогноз. Мы вычисляемпотериили ошибку(E)нашего прогноза на основе значения истинности, используя указанную функцию потерь. . Для этой классификации хорошо подходит Отрицательная потеря журнала, поэтому мы используем его. Мы вычисляем градиентдля каждого веса с помощью оптимизаторов модели, а затем корректируем веса в соответствии с градиентом. Мы используем значения скорости обучения и импульсавместе с градиентом для корректировки веса.

Обновленные веса задаются как w(new)=w(old)-Δw

def train_a_batch(net,opt,loss_fn,n_points,device='cpu'):
  net.train().to(device)  #sets the mode to training
  opt.zero_grad()         #resetting all grad values to zero
  rev,rat,batched_input,batched_output,reviews_len =
batched_dataloader(n_points,X_train,Y_train)
  output,hidden=net(batched_input,rev_len=reviews_len)
  loss=loss_fn(output,batched_output)
  loss.backward()
  opt.step()
  return loss

def train_setup(net,opt,lr=0.01,n_batches=100,batch_size=10,momentum=0.05,display_frequency=5,device='cpu',model_num='zomato A'):
  net=net.to(device)
  loss_fn=nn.NLLLoss()
  loss_plot=[]
  loss_arr=np.zeros(n_batches)
  for i in tqdm(range(n_batches),desc='Batch Completion'): 
    loss_arr[i]=train_a_batch(net,opt,loss_fn,batch_size,device)
    if i%display_frequency==0:
    loss_plot.append(loss_arr[i])
    plt.plot(loss_plot)
    plt.show()
    
    if (i+1)%50==0:
      PATH="Your selected path"-"+str(i)
      torch.save(net.state_dict(), PATH)
      print("model saved version : ",(i+1)/50)
  return loss_arr

Мы строим прогноз потери нашей модели во время обучения. Мы также сохраняем состояния нашей модели в определенных контрольных точках при обучении, чтобы позже оценить нашу модель в этих контрольных точках.

функция оценки: для оценки обученной модели мы предсказываем класс для каждого обзора в тестовом наборе. На основе значения истинности мы находим точность нашего прогноза. Чтобы детально проанализировать производительность нашей модели, мы построили матрицу путаницы для каждого из классов. Мы также рассчитываем точность, полноту и показатель f1.

def eval(net,n_points,X_,Y_,device='cpu'):
  y_true,y_pred=[],[]
  net=net.eval().to(device)
  data=dataloader(n_points,X_,Y_)
  correct=0
  for sen, senti in data:
    batched_review=batched_review_rep([sen],len(sen))
    output,hidden=net(batched_review)
    pred=torch.argmax(output)
    y_true.append(senti)
    y_pred.append(pred)
    if(pred==senti):
      correct+=1
  
  confusion=confusion_matrix(y_true, y_pred)
  confusion=confusion/confusion.astype(np.float).sum(axis=1)
  df_cm=pd.DataFrame(confusion)
  sns.heatmap(df_cm, annot=True)
  plt.show()
  target_names = ['class 0', 'class 1', 'class 2']
  print(classification_report(y_true, y_pred,
  target_names=target_names))
  accuracy=correct/n_points
  return accuracy

Настройка гиперпараметров

Теперь, когда у нас есть все ингредиенты для обучения нашей модели, мы можем начать обучение нашей модели. Сначала мы определяем нашу модель с ее гиперпараметром, а именно:

  • количество слоев LSTM
  • скрытый размер единицы LSTM
  • размер встроенного размера
  • функция потерь.
  • скорость обучения
  • импульс
  • размер партии
  • оптимизатор
#Define Hyperparameters
word_size=len(words)
n_hidden=256
num_layers=2
embedding_dim=100
loss_fn=nn.NLLLoss()
#Model Instance creation net_BiLSTM=BiLSTM_net(word_size,n_hidden,num_layers,output_size=3,embedding_dim=embedding_dim,dropout=0.5)

Теперь мы используем предварительно обученные веса встраивания для слов нашего корпусного словаря. Но мы не копируем все веса слов, присутствующие в предварительно обученных вложениях, в наше модельное вложение слов. Также могут быть некоторые слова, характерные для корпуса, которые могут отсутствовать в предварительно обученной модели. Итак, мы копируем веса тех слов, которые присутствуют в нашем корпусном словаре.

for index,word in enumerate(words):
  if word in vocab:         #vocab->words in pre-trained glove model
    ind=vocab.index(word)
    emb=torch.tensor(embeddings[ind])
    net_BiLSTM.embedding.weight.data[index]=emb

Теперь определяем оптимизаторы и начинаем обучение. Вы можете пойти прогуляться или, если вам действительно нечего делать :( , вы можете проанализировать график потерь в реальном времени во время тренировки. Вы можете остановить тренировку и принять меры, если вы чувствуете что-то подозрительное с графиком потерь.

opt=optim.Adam(net_BiLSTM.parameters(),lr=0.001)
loss_array4=train_setup(net_BiLSTM,opt,n_batches=500,batch_size=512,momentum=0,display_frequency=2,device=device_gpu,model_num='phase sep-1 zomato median 4')

Мы можем поиграть со значениями гиперпараметров, чтобы уменьшить функцию потерь, если мы чувствуем, что наше значение потерь недостаточно низкое. Как только мы будем удовлетворены, мы можем оценить нашу модель на данных проверки.

accuracy=eval(net_BiLSTM,1000,X_val,Y_val,device=device_gpu)
print("Accuracy:" ,accuracy)

Мы фиксируем различное состояние модели на разных контрольных точках и оцениваем ее производительность. Это может помочь нам обнаружить чрезмерно обученные модели, которые плохо работают с проверочными данными(переоснащение). В этом случае мы используем модели с более низкими контрольными точками, которые менее обучены.

Наконец, когда мы удовлетворены производительностью нашей модели на проверочных данных, мы оцениваем нашу модель на тестовых данных, которые неизвестны модели. Результаты этого дают нам оценку достоверности нашей модели для проблемы анализа настроений.

На этом мы завершаем нашу экспедицию по серии наборов данных Zomato.

Вы можете перейти по ссылке, чтобы найти блокнот, содержащий коды, упомянутые в этом блоге. Используемые данные принадлежат Zomato Ltd и извлекаются компанией Himanshu Poddar. Пожалуйста, укажите необходимые кредиты, если вы используете данные.