دسته‌بندی نشده

پردازش و دسته بندی متن نظرات کاربران با شبکه عصبی بازگشتی

پردازش-و-دسته-بندی-متن-نظرات-کاربران-با-شبکه-عصبی-بازگشتی

پردازش متون یکی از پر استفاده ترین کاربردهای امروزی هوش مصنوعی است که موارد استفاده متعددی مانند تحلیل اخبار، ساختن چت بات، ربات های دستیار شخصی و… دارد.

برای پردازش متن مانند تحلیل نمودارهای سری زمانی ما نیاز به بررسی کردن یک دنباله را داریم. جملات تشکیل شده از دنباله ای از کلماتند که از تعداد زیادی کاراکتر به وجود آمده اند.

برای پردازش دنباله کلمات یک متن ما نیاز به استفاده از شبکه های عصبی بازگشتی یا همان RNN ها را داریم؛

ویژگی RNN ها در این است که کلمات قبلی جمله نیز در میزان اهمیت آن تاثیر گذار است. مثلا در مدلسازی زبان، با دادن یک دنباله از کلمات به شبکه عصبی بازگشتی کلمه بعدی را پیش‌بینی میکنیم(اتفاقی که هنگام سرچ یک عبارت در گوگل می‌افتد)

دسته‌بندی و تحلیل نظرات کاربران سایت

در این مطلب قصد داریم که با استفاده از شبکه های بازگشتی، با پردازش نظرات کاربران آن ها را بر اساس احساسات دسته بندی کنیم.

برای اینکار میخوام از دیتاست کاربران سایت IMDB که یکی از بزرگترین سایت های تحلیل فیلم جهان است استفاده کنیم. این دیتاست شامل 50000 نظر در رابطه با فیلم هاست. با بررسی متون و کلمات دیدگاه های کاربران، نهایتا سیستمی خواهیم ساخت که نظر آن ها را در دو دسته مثبت و منفی دسته بندی کند؛

مراحل-پردازش-متن-با-شبکه-بازگشتی

1.متن خام

ابتدا متون مربوط به نظرات کاربران را فراخوانی میکنیم و پیش پردازش های لازم را بر روی متن آن ها انجام میدهیم؛

2.توکن گذاری

توکن گذاری به معنای جداسازی متن ها میباشد، ما در این پروژه نیاز داریم که بر اساس کلمات، متن ها را توکن بندی کنیم. یعنی جملات را بصورت کلمه به کلمه درمیاوریم.

3.تعبیه سازی

در تعبیه سازی یا embedding کلمات توکن گذاری شده تبدیل به یک بردار می شوند. دو کلمه ای که مشابه با هم باشند، بردار واژگان شبیه تری نسبت به هم دارند و ضرب داخلی این بردار ها عددی کوچک و نزدیک به صفر میشود. برای کلماتی که تشابه کمتری دارند، این موارد بصورت معکوس برقراراست؛

4.شبکه برگشتی

بعد از لایه embedding نوبت استفاده از شبکه عصبی برگشتی یا RNN است. البته برای عملکرد بهتر و سرعت بیشتر، از معماری های جدیدتر شبکه های بازگشتی مانند LSTM یا GRU استفاده خواهیم کرد.

5.مشخص‌کردن کلاس

و در آخر شبکه باید به ما دسته ی مناسب برای هر نظر را برگرداند. نظرات مثبت با برچسب مثبت و نظرات منفی با برچسب منفی.

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

در توضیحات بالا تا حدودی با ساختار یک شبکه عصبی بازگشتی آشناشدید. قصد داریم یک نمونه دسته بندی متن را با استفاده از زبان پایتون و کتابخانه های هوش مصنوعی مانند پایتورچ و همینطور کتابخانه پردازش متن spacy کدنویسی کنیم؛

نکته: کدها و تصاویر استفاده شده در مطلب، از گیت‌هاب استادرضوی برداشته شده است (لینک)

وارد کردن کتابخانه های پایتون

مانند هر پروژه ای ابتدا نیاز به وارد کردن کتابخانه های مورد نیاز برای پیش پردازش داده ها و ساخت شبکه عصبی داریم:

import os
import re
import sys
import spacy
import pickle
import numpy as np

from glob import glob
from tqdm import tqdm_notebook

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset

from utils import *
from data_utils import Vocabulary, tokenizer
from train_utils import train

# setup
use_gpu = torch.cuda.is_available()
NLP = spacy.load('en')  # NLP toolkit

تکه کد spacy.load(‘en’) که با نام NLP فراخوانی شده، مربوط پردازش متن های انگلیسی را انجام میدهد

فراخوانی دیتاست متن نظرات

دیتاست IMDB را از طریق انتخاب فولدر قرار گرفته در کامپیوتر فراخوانی میکنیم (لینک دانلود دیتاست)

data_dir = 'D:/datasets/aclImdb/dev'

vocab_path = 'vocab.pkl'

# parameters
max_len = 200
min_count = 10
batch_size = 50

 بررسی آمار نظرات داخل دیتاست

نیاز به یکسری اطلاعات آماری در رابطه با متن نظرات و تعداد کلماتشان داریم:

all_filenames = glob(f'{data_dir}/*/*/*.txt')
num_words = [len(open(f).read().split(' ')) for f in tqdm_notebook(all_filenames)]

# print statistics
print('Min length =', min(num_words))
print('Max length =', max(num_words))

print('Mean = {:.2f}'.format(np.mean(num_words)))
print('Std  = {:.2f}'.format(np.std(num_words)))

print('mean + 2 * sigma = {:.2f}'.format(np.mean(num_words) + 2.0 * np.std(num_words)))

پیش پردازش دیتاست

PAD = '<pad>'  # special symbol we use for padding text
UNK = '<unk>'  # special symbol we use for rare or unknown word

کاراکتر PAD: یک پدینگ به اول نظرات کوتاه اضافه میکند تا تعداد توکن های همه نظرات برابر باشند؛

کاراکتر unk: به جای کلماتی که کمتر از تعداد خاصی در دیتاست بکار رفته باشند(مثلا 10بار) استفاده میشود؛

class TextClassificationDataset(Dataset):
    
    def __init__(self, path, tokenizer, 
                 split='train', 
                 vocab_path='vocab.pkl', 
                 max_len=100, min_count=10):
        
        self.path = path
        assert split in ['train', 'test']
        self.split = split
        self.vocab_path = vocab_path
        self.tokenizer = tokenizer
        self.max_len = max_len
        self.min_count = min_count
        
        self.cache = {}
        self.vocab = None
        
        self.classes = []
        self.class_to_index = {}
        self.text_files = []
        
        split_path = f'{path}/{split}'
        for cls_idx, label in enumerate(os.listdir(split_path)):
            text_files = [(fname, cls_idx) for fname in glob(f'{split_path}/{label}/*.txt')]
            self.text_files += text_files
            self.classes += [label]
            self.class_to_index[label] = cls_idx
        
        self.num_classes = len(self.classes)
            
        # build vocabulary from training and validation texts
        self.build_vocab()
        
    def __getitem__(self, index):
        # read the tokenized text file and its label (neg=0, pos=1)
        fname, class_idx = self.text_files[index]
        
        if fname in self.cache:
            return self.cache[fname], class_idx
        
        # read text file 
        text = open(fname).read()
        
        # tokenize the text file
        tokens = self.tokenizer(text.lower())
        
        # padding and trimming
        if len(tokens) < self.max_len:
            num_pads = self.max_len - len(tokens)
            tokens = [PAD] * num_pads + tokens
        elif len(tokens) > self.max_len:
            tokens = tokens[:self.max_len]
            
        # numericalizing
        ids = torch.LongTensor(self.max_len)
        for i, word in enumerate(tokens):
            if word not in self.vocab.word2index:
                ids[i] = self.vocab.word2index[UNK]  # unknown words
            elif word != PAD and self.vocab.word2count[word] < self.min_count:
                ids[i] = self.vocab.word2index[UNK]  # rare words
            else:
                ids[i] = self.vocab.word2index[word]
                
        # save in cache for future use
        self.cache[fname] = ids
        
        return ids, class_idx
    
    def __len__(self):
        return len(self.text_files)
    
    def build_vocab(self):
        if not os.path.exists(self.vocab_path):
            vocab = Vocabulary(self.tokenizer)
            filenames = glob(f'{path}/*/*/*.txt')
            for filename in tqdm(filenames, desc='Building Vocab'):
                with open(filename, encoding='utf8') as f:
                    for line in f:
                        vocab.add_sentence(line.lower())

            # sort words by their frequencies
            words = [(0, PAD), (0, UNK)]
            words += sorted([(c, w) for w, c in vocab.word2count.items()], reverse=True)

            self.vocab = Vocabulary(self.tokenizer)
            for i, (count, word) in enumerate(words):
                self.vocab.word2index[word] = i
                self.vocab.word2count[word] = count
                self.vocab.index2word[i] = word
                self.vocab.count += 1

            pickle.dump(self.vocab, open(self.vocab_path, 'wb'))
        else:
            self.vocab = pickle.load(open(self.vocab_path, 'rb'))
train_ds = TextClassificationDataset(data_dir, tokenizer, 'train', vocab_path, max_len, min_count)
train_dl = DataLoader(train_ds, batch_size=batch_size, shuffle=True)

valid_ds = TextClassificationDataset(data_dir, tokenizer, 'test', vocab_path, max_len, min_count)
valid_dl = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)

Vovcabulary size

vocab = train_ds.vocab
freqs = [(count, word) for (word, count) in vocab.word2count.items() if count >= min_count]
vocab_size = len(freqs) + 2  # for PAD and UNK tokens
print(f'Vocab size = {vocab_size}')

print('\nMost common words:')
for c, w in sorted(freqs, reverse=True)[:10]:
    print(f'{w}: {c}')

کد بالا کلماتی که بیشتر از همه در نظرات بکار رفته اند را نمایش میدهد

ساخت مدل LSTM برای دسته‌بندی کردن نظرات

class LSTMClassifier(nn.Module):
    def __init__(self, embed_size, hidden_size, vocab_size, num_layers, num_classes, batch_size):
        super(LSTMClassifier, self).__init__()
        
        self.embed_size = embed_size
        self.hidden_size = hidden_size
        self.batch_size = batch_size
        self.num_layers = num_layers
        
        self.embedding = nn.Embedding(vocab_size, embed_size) # a lookup table
        self.lstm = nn.LSTM(embed_size, hidden_size, num_layers, dropout=0.3, bidirectional=True)
        self.fc = nn.Sequential(
            nn.Linear(2*hidden_size, 100),
            nn.ReLU(),
            nn.Dropout(p=0.2),
            nn.Linear(100, num_classes)
        )
        self.hidden = self.init_hidden()
    
    def init_hidden(self):
        h = to_var(torch.zeros((2*self.num_layers, self.batch_size, self.hidden_size)))
        c = to_var(torch.zeros((2*self.num_layers, self.batch_size, self.hidden_size)))
        return h, c
    
    def forward(self, x):
        x = self.embedding(x)
        x, self.hidden = self.lstm(x, self.hidden)
        x = self.fc(x[-1])  # select the last output
        return x
# LSTM parameters
embed_size = 100
hidden_size = 256
num_layers = 1

# training parameters
lr = 0.001
num_epochs = 10
model = LSTMClassifier(embed_size=embed_size, 
                       hidden_size=hidden_size, 
                       vocab_size=vocab_size,
                       num_layers=num_layers,
                       num_classes=train_ds.num_classes, 
                       batch_size=batch_size)

if use_gpu:
    model = model.cuda()
criterion = nn.CrossEntropyLoss()
if use_gpu:
    criterion = criterion.cuda()
    
optimizer = optim.Adam(model.parameters(), lr=lr, betas=(0.7, 0.99))
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=1, gamma=0.975)

آموزش دادن مدل

با اجرای کد زیر مدل شروع به آموزش دیدن میکند (پارامترها قابل تغییر هستند)

hist = train(model, train_dl, valid_dl, criterion, optimizer, scheduler, num_epochs)

همینطور میتوان مدل های آموزش دیده از قبل را فراخوانی کرد

# LSTM parameters
max_len = 400
min_count = 10
embed_size = 256
hidden_size = 1024
num_layers = 1


model = LSTMClassifier(embed_size=embed_size, 
                       hidden_size=hidden_size, 
                       vocab_size=vocab_size,
                       num_layers=num_layers,
                       num_classes=train_ds.num_classes, 
                       batch_size=batch_size)

model.load_state_dict(torch.load('models/tmp/lstm-1-400-10-256-1024-1-0.87964.pth'))

if use_gpu:
    model = model.cuda()

 نکته های تکمیلی

  1. برای دسترسی کاملتر و راحت تر به کد های پروژه، بهتر است از سورس اصلی که ابتدا معرفی شد استفاده کنید.
  2. با استفاده از تکنیک یادگیری انتقالی(ترانسفرلرنینگ) میتوان از بردار واژگان از پیش آماده شده (بطور مثال مدل های آماده گوگل و یا فیسبوک) استفاده کرد و با دقت بهتری پیش بینی انجام داد.
  3. LSTM میتواند دوطرفه نیز باشد، یعنی هم از ایتدا و هم از انتها متن را بخواند و پردازش کند.
  4. معماری GRU، شبکه بازگشتی ساده سازی تر شده ای نسبت به LSTM است که پردازش را سریعتر انجام میدهد.
  5. مکانیزم Attention: این مکانیزم میزان توجه هر کلمه نسبت به سایر کلمات را بررسی میکند(خروجی یک کلمه در ترجمه فارسی، به کدام کلمات در جمله انگلیسی وابستگی بیشتری دارد)
  6. شبکه های QRNN: ترکیب ایده های شبکه بازگشتی و کانولوشنال است که سرعت بسیار بالاتری نسبت به RNNها دارد.
author-avatar

درباره محمد اسماعیلی

علاقه مند به مفاهیم هوش مصنوعی، دیتاساینس و سئو؛ مطالبی که برام جالب باشه رو اینجا می نویسم، و این دلیل بر متخصص بودن من در اون حوزه ها نمیشه😊

نوشته های مرتبط

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *