پردازش متون یکی از پر استفاده ترین کاربردهای امروزی هوش مصنوعی است که موارد استفاده متعددی مانند تحلیل اخبار، ساختن چت بات، ربات های دستیار شخصی و… دارد.
برای پردازش متن مانند تحلیل نمودارهای سری زمانی ما نیاز به بررسی کردن یک دنباله را داریم. جملات تشکیل شده از دنباله ای از کلماتند که از تعداد زیادی کاراکتر به وجود آمده اند.
برای پردازش دنباله کلمات یک متن ما نیاز به استفاده از شبکه های عصبی بازگشتی یا همان RNN ها را داریم؛
ویژگی RNN ها در این است که کلمات قبلی جمله نیز در میزان اهمیت آن تاثیر گذار است. مثلا در مدلسازی زبان، با دادن یک دنباله از کلمات به شبکه عصبی بازگشتی کلمه بعدی را پیشبینی میکنیم(اتفاقی که هنگام سرچ یک عبارت در گوگل میافتد)
دستهبندی و تحلیل نظرات کاربران سایت
در این مطلب قصد داریم که با استفاده از شبکه های بازگشتی، با پردازش نظرات کاربران آن ها را بر اساس احساسات دسته بندی کنیم.
برای اینکار میخوام از دیتاست کاربران سایت IMDB که یکی از بزرگترین سایت های تحلیل فیلم جهان است استفاده کنیم. این دیتاست شامل 50000 نظر در رابطه با فیلم هاست. با بررسی متون و کلمات دیدگاه های کاربران، نهایتا سیستمی خواهیم ساخت که نظر آن ها را در دو دسته مثبت و منفی دسته بندی کند؛
1.متن خام
ابتدا متون مربوط به نظرات کاربران را فراخوانی میکنیم و پیش پردازش های لازم را بر روی متن آن ها انجام میدهیم؛
2.توکن گذاری
توکن گذاری به معنای جداسازی متن ها میباشد، ما در این پروژه نیاز داریم که بر اساس کلمات، متن ها را توکن بندی کنیم. یعنی جملات را بصورت کلمه به کلمه درمیاوریم.
3.تعبیه سازی
در تعبیه سازی یا embedding کلمات توکن گذاری شده تبدیل به یک بردار می شوند. دو کلمه ای که مشابه با هم باشند، بردار واژگان شبیه تری نسبت به هم دارند و ضرب داخلی این بردار ها عددی کوچک و نزدیک به صفر میشود. برای کلماتی که تشابه کمتری دارند، این موارد بصورت معکوس برقراراست؛
4.شبکه برگشتی
بعد از لایه embedding نوبت استفاده از شبکه عصبی برگشتی یا RNN است. البته برای عملکرد بهتر و سرعت بیشتر، از معماری های جدیدتر شبکه های بازگشتی مانند LSTM یا GRU استفاده خواهیم کرد.
5.مشخصکردن کلاس
و در آخر شبکه باید به ما دسته ی مناسب برای هر نظر را برگرداند. نظرات مثبت با برچسب مثبت و نظرات منفی با برچسب منفی.
کدنویسی پروژه با زبان پایتون
در توضیحات بالا تا حدودی با ساختار یک شبکه عصبی بازگشتی آشناشدید. قصد داریم یک نمونه دسته بندی متن را با استفاده از زبان پایتون و کتابخانه های هوش مصنوعی مانند پایتورچ و همینطور کتابخانه پردازش متن spacy کدنویسی کنیم؛
نکته: کدها و تصاویر استفاده شده در مطلب، از گیتهاب استادرضوی برداشته شده است (لینک)
وارد کردن کتابخانه های پایتون
مانند هر پروژه ای ابتدا نیاز به وارد کردن کتابخانه های مورد نیاز برای پیش پردازش داده ها و ساخت شبکه عصبی داریم:
[py]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[/py]
تکه کد spacy.load(‘en’) که با نام NLP فراخوانی شده، مربوط پردازش متن های انگلیسی را انجام میدهد
فراخوانی دیتاست متن نظرات
دیتاست IMDB را از طریق انتخاب فولدر قرار گرفته در کامپیوتر فراخوانی میکنیم (لینک دانلود دیتاست)
[py]data_dir = ‘D:/datasets/aclImdb/dev’
vocab_path = ‘vocab.pkl’
# parameters
max_len = 200
min_count = 10
batch_size = 50[/py]
بررسی آمار نظرات داخل دیتاست
نیاز به یکسری اطلاعات آماری در رابطه با متن نظرات و تعداد کلماتشان داریم:
[py]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)))[/py]
پیش پردازش دیتاست
[py]PAD = ‘<pad>’ # special symbol we use for padding text
UNK = ‘<unk>’ # special symbol we use for rare or unknown word[/py]
کاراکتر PAD: یک پدینگ به اول نظرات کوتاه اضافه میکند تا تعداد توکن های همه نظرات برابر باشند؛
کاراکتر unk: به جای کلماتی که کمتر از تعداد خاصی در دیتاست بکار رفته باشند(مثلا 10بار) استفاده میشود؛
[py]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’))[/py]
[py]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)[/py]
Vovcabulary size
[py]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}’)[/py]
کد بالا کلماتی که بیشتر از همه در نظرات بکار رفته اند را نمایش میدهد
ساخت مدل LSTM برای دستهبندی کردن نظرات
[py]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[/py]
[py]# LSTM parameters
embed_size = 100
hidden_size = 256
num_layers = 1
# training parameters
lr = 0.001
num_epochs = 10[/py]
[py]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()[/py]
[py]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)[/py]
آموزش دادن مدل
با اجرای کد زیر مدل شروع به آموزش دیدن میکند (پارامترها قابل تغییر هستند)
[py]hist = train(model, train_dl, valid_dl, criterion, optimizer, scheduler, num_epochs)[/py]
همینطور میتوان مدل های آموزش دیده از قبل را فراخوانی کرد
[py]# 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()[/py]
نکته های تکمیلی
- برای دسترسی کاملتر و راحت تر به کد های پروژه، بهتر است از سورس اصلی که ابتدا معرفی شد استفاده کنید.
- با استفاده از تکنیک یادگیری انتقالی(ترانسفرلرنینگ) میتوان از بردار واژگان از پیش آماده شده (بطور مثال مدل های آماده گوگل و یا فیسبوک) استفاده کرد و با دقت بهتری پیش بینی انجام داد.
- LSTM میتواند دوطرفه نیز باشد، یعنی هم از ایتدا و هم از انتها متن را بخواند و پردازش کند.
- معماری GRU، شبکه بازگشتی ساده سازی تر شده ای نسبت به LSTM است که پردازش را سریعتر انجام میدهد.
- مکانیزم Attention: این مکانیزم میزان توجه هر کلمه نسبت به سایر کلمات را بررسی میکند(خروجی یک کلمه در ترجمه فارسی، به کدام کلمات در جمله انگلیسی وابستگی بیشتری دارد)
- شبکه های QRNN: ترکیب ایده های شبکه بازگشتی و کانولوشنال است که سرعت بسیار بالاتری نسبت به RNNها دارد.