دسته بندی موجودیت های نام دار (Named Entity) — راهنمای کاربردی

شناسایی موجودیت های نام دار (Named Entity Recognition | NER) و دستهبندی آنها (Classification)، فرآیند تشخیص واحدهای اطلاعاتی مانند اسامی افراد، سازمانها و مکانها، و همچنین بیانات عددی از متن ساختارنیافته است. هدف از این کار توسعه روشهای کاربردی و مستقل از دامنه به منظور شناسایی موجودیتهای نامدار با صحت بالا به صورت خودکار است.
شناسایی و دستهبندی موجودیتهای نامدار (Named Entity Recognition and Classification | NERC) فرآیند تشخیص واحدهای اطلاعاتی مانند اسامی افراد، سازمانها، موقعیتها و بیانات ریاضی مانند زمان، تاریخ، پول و درصد در دادههای ساختارنیافته است. هدف از این کار توسعه روشهای کاربردی و مستقل از دامنه به منظور شناسایی موجودیتهای نامدار، با صحت بالا و به صورت خودکار محسوب میشود.
در مطلب «شناسایی موجودیت نام دار با NLTK و SpaCy — راهنمای کاربردی»، مقدمهای بر NLTK و SpaCy، مفاهیم موجودیت نامدار و چگونگی تشخیص آن با استفاده از دو ابزار بیان شده، مورد بررسی قرار گرفت. اکنون، یک گام به جلوتر رفته و مدل یادگیری ماشین برای NER با استفاده از کتابخانه Scikit-Learn آموزش داده خواهد شد.
داده
دادههای مورد استفاده در اینجا مجموعه نوشتههای مهندسی ویژگی شده با تگهای IOB و POS هستند که در «Kaggle» (+) وجود دارند. در تصویر زیر چند سطر اول این مجموعه داده مشهود است.
اطلاعات اساسی پیرامون موجودیتها:
- geo = Geographical Entity (موجودیت جغرافیایی)
- org = Organization (سازمان)
- per = Person (شخص)
- gpe = Geopolitical Entity (موجودیت ژئوپلتیکی)
- tim = Time indicator (شاخص زمانی)
- art = Artifact (مصنوعات)
- eve = Event (رویداد)
- nat = Natural Phenomenon (پدیده طبیعی)
IOB
«IOB» سرنامی برای عبارت «Inside–outside–beginning» است و یک روش تگ زنی متداول برای تگ زدن توکنها محسوب میشود.
- I: پیشوند پیش از آنکه تگ نشان دهد که درون یک بخش است.
- B: پیشوندی که نشان میدهد یک تگ آغاز یک بخش است.
- O: این تگ نشان میدهد که یک توکن متعلق به هیچ بخشی نیست (بیرون).
import pandas as pd import numpy as np from sklearn.feature_extraction import DictVectorizer from sklearn.feature_extraction.text import HashingVectorizer from sklearn.linear_model import Perceptron from sklearn.model_selection import train_test_split from sklearn.linear_model import SGDClassifier from sklearn.linear_model import PassiveAggressiveClassifier from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report
کل مجموعه داده در حافظه یک کامپیوتر جا نمیشود، بنابراین اولین ۱۰۰۰۰۰ رکورد آن را انتخاب کرده و از الگوریتمهای یادگیری حافظه خارجی (Out-of-core learning) به منظور «واکشی» (fetch) و پردازش دادهها استفاده میشود.
df = pd.read_csv('ner_dataset.csv', encoding = "ISO-8859-1") df = df[:100000] df.head()
df.isnull().sum()
پیشپردازش دادهها
همانطور که پیشتر مشخص شد، مقادیر NaN زیادی در ستون «# Sentence» وجود دارند و بنابراین باید این سلولها را با مقادیر دیگری جایگزین کرد.
df = df.fillna(method='ffill') df['Sentence #'].nunique(), df.Word.nunique(), df.Tag.nunique()
(4544, 10922, 17)
۴۵۴۴ جمله وجود دارد که شامل ۱۰۹۲۲ کلمه یکتا هستند و با ۱۷ تگ، برچسبگذاری شدهاند. این تگها به طور منظم توزیع نشدهاند.
df.groupby('Tag').size().reset_index(name='counts')
کد زیر تاریخ متن را با استفاده از «DictVectorizer» (+) و سپس تقسیمبندی آن به دادههای «آموزش» (train) و «آزمون» (test) به بردار تبدیل میکند.
X = df.drop('Tag', axis=1) v = DictVectorizer(sparse=False) X = v.fit_transform(X.to_dict('records')) y = df.Tag.values classes = np.unique(y) classes = classes.tolist() X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.33, random_state=0) X_train.shape, y_train.shape
((67000, 15507), (67000,))
الگوریتمهای یادگیری حافظه خارجی
در ادامه برخی از الگوریتمهای حافظه خارجی که برای پردازش دادههای بسیار بزرگ در حافظه یک کامپیوتر طراحی شدهاند و از متد partial_fit استفاده میکنند، مورد آزمون قرار میگیرند.
پرسپترون (Perceptron)
per = Perceptron(verbose=10, n_jobs=-1, max_iter=5) per.partial_fit(X_train, y_train, classes)
به دلیل آنکه تگ «O» (سرنام outside) متداولترین تگ است و موجب میشود ک نتایج بسیار بهتر از آنچه واقعا هستند به نظر بیایند، هنگام ارزیابی سنجههای دستهبندی باید این تگ را حذف کرد.
new_classes = classes.copy() new_classes.pop() new_classes
print(classification_report(y_pred=per.predict(X_test), y_true=y_test, labels=new_classes))
دستهبندهای خطی با آموزش SGD
sgd = SGDClassifier() sgd.partial_fit(X_train, y_train, classes)
print(classification_report(y_pred=sgd.predict(X_test), y_true=y_test, labels=new_classes))
دستهبندی نایو بیز برای مدلهای چندجملهای
nb = MultinomialNB(alpha=0.01) nb.partial_fit(X_train, y_train, classes)
print(classification_report(y_pred=nb.predict(X_test), y_true=y_test, labels = new_classes))
دستهبند تهاجمی منفعل (Passive Aggressive Classifier)
pa =PassiveAggressiveClassifier() pa.partial_fit(X_train, y_train, classes)
print(classification_report(y_pred=pa.predict(X_test), y_true=y_test, labels=new_classes))
هیچ یک از دستهبندهای بالا نتایج قابل قبولی تولید نمیکنند. واضح است که هیچ چیز به آسانی دستهبندی موجودیتهای نام دار با استفاده از دستهبندهای متداول نیست ولی برای دریافت خروجی مناسب باید تلاش بیشتری کرد.
میدان تصادفی شرطی
«میدان تصادفی شرطی» (Conditional Random Field | CRF)، اغلب برای برچسبگذاری یا «تجزیه» (parsing) «دادههای متوالی» (Sequential Data) مانند پردازش زبان طبیعی مورد استفاده قرار میگیرد و در «برچسبگذاری اجزای کلام» (Part-of-speech tagging)، شناسایی موجودیتهای نامگذاری شده و دیگر موارد کاربرد دارد.
sklearn-crfsuite
یک مدل CRF برای شناسایی موجودیتهای نامدار با استفاده از sklearn-crfsuite روی مجموعه داده موجود، آموزش داده میشود.
import sklearn_crfsuite from sklearn_crfsuite import scorers from sklearn_crfsuite import metrics from collections import Counter
کد زیر به منظور بازیابی جملات با POS و تگهای آنها مورد استفاده قرار میگیرد. (نکات لازم در این رابطه از اینجا (+) قابل مطالعه است.)
class SentenceGetter(object): def __init__(self, data): self.n_sent = 1 self.data = data self.empty = False agg_func = lambda s: [(w, p, t) for w, p, t in zip(s['Word'].values.tolist(), s['POS'].values.tolist(), s['Tag'].values.tolist())] self.grouped = self.data.groupby('Sentence #').apply(agg_func) self.sentences = [s for s in self.grouped] def get_next(self): try: s = self.grouped['Sentence: {}'.format(self.n_sent)] self.n_sent += 1 return s except: return None getter = SentenceGetter(df) sentences = getter.sentences
استخراج ویژگی
در این وهله، ویژگیهای بیشتری (بخشهای کلمه، تگهای POS ساده شده، فلگهای پایینتر/عنوان/بالاتر، ویژگیهای کلمات موجود در نزدیکی) استخراج و به فرمت sklearn-crfsuite تبدیل میشوند. هر جمله باید به لیستی از احکام تبدیل شود. کد زیر از وبسایت رسمی sklearn-crfsuites (+) دریافت شده است.
def word2features(sent, i): word = sent[i][0] postag = sent[i][1] features = { 'bias': 1.0, 'word.lower()': word.lower(), 'word[-3:]': word[-3:], 'word[-2:]': word[-2:], 'word.isupper()': word.isupper(), 'word.istitle()': word.istitle(), 'word.isdigit()': word.isdigit(), 'postag': postag, 'postag[:2]': postag[:2], } if i > 0: word1 = sent[i-1][0] postag1 = sent[i-1][1] features.update({ '-1:word.lower()': word1.lower(), '-1:word.istitle()': word1.istitle(), '-1:word.isupper()': word1.isupper(), '-1:postag': postag1, '-1:postag[:2]': postag1[:2], }) else: features['BOS'] = True if i < len(sent)-1: word1 = sent[i+1][0] postag1 = sent[i+1][1] features.update({ '+1:word.lower()': word1.lower(), '+1:word.istitle()': word1.istitle(), '+1:word.isupper()': word1.isupper(), '+1:postag': postag1, '+1:postag[:2]': postag1[:2], }) else: features['EOS'] = True return features def sent2features(sent): return [word2features(sent, i) for i in range(len(sent))] def sent2labels(sent): return [label for token, postag, label in sent] def sent2tokens(sent): return [token for token, postag, label in sent]
جداسازی مجموعههای آموزش و آزمون
X = [sent2features(s) for s in sentences] y = [sent2labels(s) for s in sentences] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=0)
آموزش یک مدل CRF
crf = sklearn_crfsuite.CRF( algorithm='lbfgs', c1=0.1, c2=0.1, max_iterations=100, all_possible_transitions=True ) crf.fit(X_train, y_train)
ارزیابی
y_pred = crf.predict(X_test) print(metrics.flat_classification_report(y_test, y_pred, labels = new_classes))
به نظر میرسد نتایج بهبود یافته است. بنابراین با استفاده از sklearn-crfsuite باید به اکتشافات بیشتری پرداخت.
دستهبند چه چیزی یاد گرفته؟
def print_transitions(trans_features): for (label_from, label_to), weight in trans_features: print("%-6s -> %-7s %0.6f" % (label_from, label_to, weight)) print("Top likely transitions:") print_transitions(Counter(crf.transition_features_).most_common(20)) print("\nTop unlikely transitions:") print_transitions(Counter(crf.transition_features_).most_common()[-20:])
تفسیر: احتمال زیادی وجود دارد که شروع یک موجودیت جغرافیایی (beginning of a geographical entity | B-geo) با یک توکن درون موجودیت جغرافیایی (inside geographical entity | I-geo) دنبال شود. اما انتقال به داخل نام یک سازمان (inside of an organization | I-org) از توکنهایی با دیگر برچسبها دارای سختی زیادی است.
بررسی ویژگیهای وضعیت
def print_state_features(state_features): for (attr, label), weight in state_features: print("%0.6f %-8s %s" % (weight, label, attr)) print("Top positive:") print_state_features(Counter(crf.state_features_).most_common(30)) print("\nTop negative:") print_state_features(Counter(crf.state_features_).most_common()[-30:])
مشاهدات
- 5.183603 B-tim word[-3]:day: مدل میآموزد که کلمهای که در نزدیکی قرار داشته «day» بوده، بنابراین توکن احتمالا بخشی از شاخص زمانی است.
- 3.370614 B-per word.lower():president: مدل میآموزد که توکن «president» احتمالا در ابتدای نام یک فرد قرار دارد.
- -3.521244 O postag:NNP: مدل میآموزد که اسامی خاص معمولا موجودیتها هستند.
- -3.087828 O word.isdigit(): ارقام، موجودیتهای احتمالی هستند.
- -3.233526 O word.istitle(): کلمات TitleCase موجودیتهای احتمالی هستند.
ELI5
ELI5 یک بسته پایتون است که امکان بررسی وزنهای مدلهای sklearn_crfsuite.CRF را میدهد.
بازبینی وزنهای مدل
import eli5 eli5.show_weights(crf, top=10)
مشاهدات
- این موضوع که I-entity باید B-entity را دنبال کند دارای معنا است، مثلا I-geo در اینجا B-geo را، I-org هم B-org را و I-per نیز B-per را دنبال میکند.
- میتوان مشاهده کرد که یک سازمان درست بعد از نام یک فرد بیاید (B-org -> I-per دارای وزن منفی بسیار بالایی است).
- مدل وزنهای منفی بزرگ را برای انتقال غیر ممکن مانند O -> I-geo ،O -> I-org و O -> I-tim میآموزد.
برای آسان شدن خواندن، میتوان تنها یک زیرمجموعه از تگها را بررسی کرد.
eli5.show_weights(crf, top=10, targets=['O', 'B-org', 'I-per'])
و یا صرفا برخی از ویژگیها برای همه تگها را مورد بررسی قرار داد.
eli5.show_weights(crf, top=10, feature_re='^word\.is', horizontal_layout=False, show=['targets'])
کد کامل انجام این کار با استفاده از sklearn-crfsuite و ELI5 در گیتهاب (+) موجود است.
اگر مطلب بالا برای شما مفید بوده، آموزشهای زیر نیز به شما پیشنهاد میشود:
- مجموعه آموزشهای آمار، احتمالات و دادهکاوی
- مجموعه آموزشهای یادگیری ماشین و بازشناسی الگو
- گنجینه آموزشهای برنامه نویسی پایتون (Python)
- spaCy در پایتون — پردازش زبان طبیعی به صورت آسان
- مجموعه آموزشهای شبکههای عصبی مصنوعی
- مجموعه آموزشهای هوش محاسباتی
- آموزش برنامهنویسی R و نرمافزار R Studio
- مجموعه آموزشهای برنامه نویسی متلب (MATLAB)
^^