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

۶۸۰ بازدید
آخرین به‌روزرسانی: ۲۴ اردیبهشت ۱۴۰۲
زمان مطالعه: ۵ دقیقه
دسته بندی موجودیت های نام دار (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: این تگ نشان می‌دهد که یک توکن متعلق به هیچ بخشی نیست (بیرون).
1import pandas as pd
2import numpy as np
3from sklearn.feature_extraction import DictVectorizer
4from sklearn.feature_extraction.text import HashingVectorizer
5from sklearn.linear_model import Perceptron
6from sklearn.model_selection import train_test_split
7from sklearn.linear_model import SGDClassifier
8from sklearn.linear_model import PassiveAggressiveClassifier
9from sklearn.naive_bayes import MultinomialNB
10from sklearn.metrics import classification_report

کل مجموعه داده در حافظه یک کامپیوتر جا نمی‌شود، بنابراین اولین ۱۰۰۰۰۰ رکورد آن را انتخاب کرده و از الگوریتم‌های یادگیری حافظه خارجی (Out-of-core learning) به منظور «واکشی» (fetch) و پردازش داده‌ها استفاده می‌شود.

1df = pd.read_csv('ner_dataset.csv', encoding = "ISO-8859-1")
2df = df[:100000]
3df.head()

Out-of-core learning algorithms

1df.isnull().sum()

Named Entity Recognition

پیش‌پردازش داده‌ها

همانطور که پیش‌تر مشخص شد، مقادیر NaN زیادی در ستون «# Sentence» وجود دارند و بنابراین باید این سلول‌ها را با مقادیر دیگری جایگزین کرد.

1df = df.fillna(method='ffill')
2
3df['Sentence #'].nunique(), df.Word.nunique(), df.Tag.nunique()
(4544, 10922, 17)

۴۵۴۴ جمله وجود دارد که شامل ۱۰۹۲۲ کلمه یکتا هستند و با ۱۷ تگ، برچسب‌گذاری شده‌اند. این تگ‌ها به طور منظم توزیع نشده‌اند.

1df.groupby('Tag').size().reset_index(name='counts')

پیش‌پردازش داده‌ها

کد زیر تاریخ متن را با استفاده از «DictVectorizer» (+) و سپس تقسیم‌بندی آن به داده‌های «آموزش» (train) و «آزمون» (test) به بردار تبدیل می‌کند.

1X = df.drop('Tag', axis=1)
2v = DictVectorizer(sparse=False)
3X = v.fit_transform(X.to_dict('records'))
4y = df.Tag.values
5
6classes = np.unique(y)
7classes = classes.tolist()
8
9X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.33, random_state=0)
10X_train.shape, y_train.shape
((67000, 15507), (67000,))

الگوریتم‌های یادگیری حافظه خارجی

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

پرسپترون (Perceptron)

1per = Perceptron(verbose=10, n_jobs=-1, max_iter=5)
2per.partial_fit(X_train, y_train, classes)

الگوریتم‌های حافظه خارجی

به دلیل آنکه تگ «O» (سرنام outside) متداول‌ترین تگ است و موجب می‌شود ک نتایج بسیار بهتر از آنچه واقعا هستند به نظر بیایند، هنگام ارزیابی سنجه‌های دسته‌بندی باید این تگ را حذف کرد.

1new_classes = classes.copy()
2new_classes.pop()
3new_classes

پیش‌پردازش داده‌ها

1print(classification_report(y_pred=per.predict(X_test), y_true=y_test, labels=new_classes))

پردازش داده‌ها

دسته‌بندهای خطی با آموزش SGD

1sgd = SGDClassifier()
2sgd.partial_fit(X_train, y_train, classes)

دسته‌بندهای خطی با آموزش گرادیان کاهشی تصادفی (SGD)

1print(classification_report(y_pred=sgd.predict(X_test), y_true=y_test, labels=new_classes))

دسته‌بندی نایو بیز برای مدل‌های چندجمله‌ای

1nb = MultinomialNB(alpha=0.01)
2nb.partial_fit(X_train, y_train, classes)

دسته‌بند نایو بیز برای مدل‌های چندجمله‌ای

1print(classification_report(y_pred=nb.predict(X_test), y_true=y_test, labels = new_classes))

دسته‌بند تهاجمی منفعل (Passive Aggressive Classifier)

1pa =PassiveAggressiveClassifier()
2pa.partial_fit(X_train, y_train, classes)

دسته‌بند Passive Aggressive

1print(classification_report(y_pred=pa.predict(X_test), y_true=y_test, labels=new_classes))

دسته‌بندی Passive Aggressive

هیچ یک از دسته‌بندهای بالا نتایج قابل قبولی تولید نمی‌کنند. واضح است که هیچ چیز به آسانی دسته‌بندی موجودیت‌های نام دار با استفاده از دسته‌بندهای متداول نیست ولی برای دریافت خروجی مناسب باید تلاش بیشتری کرد.

میدان تصادفی شرطی

«میدان تصادفی شرطی» (Conditional Random Field | CRF)، اغلب برای برچسب‌گذاری یا «تجزیه» (parsing) «داده‌های متوالی» (Sequential Data) مانند پردازش زبان طبیعی مورد استفاده قرار می‌گیرد و در «برچسب‌گذاری اجزای کلام» (Part-of-speech tagging)، شناسایی موجودیت‌های نام‌گذاری شده و دیگر موارد کاربرد دارد.

sklearn-crfsuite

یک مدل CRF برای شناسایی موجودیت‌های نام‌دار با استفاده از sklearn-crfsuite روی مجموعه داده موجود، آموزش داده می‌شود.

1import sklearn_crfsuite
2from sklearn_crfsuite import scorers
3from sklearn_crfsuite import metrics
4from collections import Counter

کد زیر به منظور بازیابی جملات با POS و تگ‌های آن‌ها مورد استفاده قرار می‌گیرد. (نکات لازم در این رابطه از اینجا (+) قابل مطالعه است.)

1class SentenceGetter(object):
2    
3    def __init__(self, data):
4        self.n_sent = 1
5        self.data = data
6        self.empty = False
7        agg_func = lambda s: [(w, p, t) for w, p, t in zip(s['Word'].values.tolist(), 
8                                                           s['POS'].values.tolist(), 
9                                                           s['Tag'].values.tolist())]
10        self.grouped = self.data.groupby('Sentence #').apply(agg_func)
11        self.sentences = [s for s in self.grouped]
12        
13    def get_next(self):
14        try: 
15            s = self.grouped['Sentence: {}'.format(self.n_sent)]
16            self.n_sent += 1
17            return s 
18        except:
19            return None
20
21getter = SentenceGetter(df)
22sentences = getter.sentences

استخراج ویژگی

در این وهله، ویژگی‌های بیشتری (بخش‌های کلمه، تگ‌های POS ساده شده، فلگ‌های پایین‌تر/عنوان/بالاتر، ویژگی‌های کلمات موجود در نزدیکی) استخراج و به فرمت sklearn-crfsuite تبدیل می‌شوند.

هر جمله باید به لیستی از احکام تبدیل شود. کد زیر از وب‌سایت رسمی sklearn-crfsuites (+) دریافت شده است.

1def word2features(sent, i):
2    word = sent[i][0]
3    postag = sent[i][1]
4    
5    features = {
6        'bias': 1.0, 
7        'word.lower()': word.lower(), 
8        'word[-3:]': word[-3:],
9        'word[-2:]': word[-2:],
10        'word.isupper()': word.isupper(),
11        'word.istitle()': word.istitle(),
12        'word.isdigit()': word.isdigit(),
13        'postag': postag,
14        'postag[:2]': postag[:2],
15    }
16    if i > 0:
17        word1 = sent[i-1][0]
18        postag1 = sent[i-1][1]
19        features.update({
20            '-1:word.lower()': word1.lower(),
21            '-1:word.istitle()': word1.istitle(),
22            '-1:word.isupper()': word1.isupper(),
23            '-1:postag': postag1,
24            '-1:postag[:2]': postag1[:2],
25        })
26    else:
27        features['BOS'] = True
28    if i < len(sent)-1:
29        word1 = sent[i+1][0]
30        postag1 = sent[i+1][1]
31        features.update({
32            '+1:word.lower()': word1.lower(),
33            '+1:word.istitle()': word1.istitle(),
34            '+1:word.isupper()': word1.isupper(),
35            '+1:postag': postag1,
36            '+1:postag[:2]': postag1[:2],
37        })
38    else:
39        features['EOS'] = True
40
41    return features
42
43def sent2features(sent):
44    return [word2features(sent, i) for i in range(len(sent))]
45
46def sent2labels(sent):
47    return [label for token, postag, label in sent]
48
49def sent2tokens(sent):
50    return [token for token, postag, label in sent]

جداسازی مجموعه‌های آموزش و آزمون

1X = [sent2features(s) for s in sentences]
2y = [sent2labels(s) for s in sentences]
3X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=0)

آموزش یک مدل CRF

1crf = sklearn_crfsuite.CRF(
2    algorithm='lbfgs',
3    c1=0.1,
4    c2=0.1,
5    max_iterations=100,
6    all_possible_transitions=True
7)
8crf.fit(X_train, y_train)

Train a CRF model

ارزیابی

1y_pred = crf.predict(X_test)
2print(metrics.flat_classification_report(y_test, y_pred, labels = new_classes))

به نظر می‌رسد نتایج بهبود یافته است. بنابراین با استفاده از sklearn-crfsuite باید به اکتشافات بیشتری پرداخت.

دسته‌بند چه چیزی یاد گرفته؟

1def print_transitions(trans_features):
2    for (label_from, label_to), weight in trans_features:
3        print("%-6s -> %-7s %0.6f" % (label_from, label_to, weight))
4
5print("Top likely transitions:")
6print_transitions(Counter(crf.transition_features_).most_common(20))
7
8print("\nTop unlikely transitions:")
9print_transitions(Counter(crf.transition_features_).most_common()[-20:])

Named Entity Recognition

تفسیر: احتمال زیادی وجود دارد که شروع یک موجودیت جغرافیایی (beginning of a geographical entity | B-geo) با یک توکن درون موجودیت جغرافیایی (inside geographical entity | I-geo) دنبال شود. اما انتقال به داخل نام یک سازمان (inside of an organization | I-org) از توکن‌هایی با دیگر برچسب‌ها دارای سختی زیادی است.

بررسی ویژگی‌های وضعیت

1def print_state_features(state_features):
2    for (attr, label), weight in state_features:
3        print("%0.6f %-8s %s" % (weight, label, attr))
4
5print("Top positive:")
6print_state_features(Counter(crf.state_features_).most_common(30))
7
8print("\nTop negative:")
9print_state_features(Counter(crf.state_features_).most_common()[-30:])

named entity recognition

مشاهدات

  1. 5.183603 B-tim word[-3]:day: مدل می‌آموزد که کلمه‌ای که در نزدیکی قرار داشته «day» بوده، بنابراین توکن احتمالا بخشی از شاخص زمانی است.
  2. 3.370614 B-per word.lower():president: مدل می‌آموزد که توکن «president» احتمالا در ابتدای نام یک فرد قرار دارد.
  3. -3.521244 O postag:NNP: مدل می‌آموزد که اسامی خاص معمولا موجودیت‌ها هستند.
  4. -3.087828 O word.isdigit(): ارقام، موجودیت‌های احتمالی هستند.
  5. -3.233526 O word.istitle(): کلمات TitleCase موجودیت‌های احتمالی هستند.

ELI5

ELI5 یک بسته پایتون است که امکان بررسی وزن‌های مدل‌های sklearn_crfsuite.CRF را می‌دهد.

بازبینی وزن‌های مدل

1import eli5
2eli5.show_weights(crf, top=10)

named entity recognition

named entity recognition

مشاهدات

  1. این موضوع که I-entity باید B-entity را دنبال کند دارای معنا است، مثلا I-geo در اینجا B-geo را، I-org هم B-org را و I-per نیز B-per را دنبال می‌کند.
  2. می‌توان مشاهده کرد که یک سازمان درست بعد از نام یک فرد بیاید (B-org -> I-per دارای وزن منفی بسیار بالایی است).
  3. مدل وزن‌های منفی بزرگ را برای انتقال غیر ممکن مانند O -> I-geo ،O -> I-org و O -> I-tim می‌آموزد.

برای آسان شدن خواندن، می‌توان تنها یک زیرمجموعه از تگ‌ها را بررسی کرد.

1eli5.show_weights(crf, top=10, targets=['O', 'B-org', 'I-per'])

named entity recognition

و یا صرفا برخی از ویژگی‌ها برای همه تگ‌ها را مورد بررسی قرار داد.

1eli5.show_weights(crf, top=10, feature_re='^word\.is',
2                  horizontal_layout=False, show=['targets'])

named entity recognition

کد کامل انجام این کار با استفاده از sklearn-crfsuite و ELI5 در گیت‌هاب (+) موجود است.

اگر مطلب بالا برای شما مفید بوده، آموزش‌های زیر نیز به شما پیشنهاد می‌شود:

^^

بر اساس رای ۴ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
kdnuggets
۱ دیدگاه برای «دسته بندی موجودیت های نام دار (Named Entity) — راهنمای کاربردی»

سلام ممنون از مطلب خوبتون.
کدی که در این صفحه قرار داده‌اید برای متن‌های فارسی هم کار می‌کند؟

نظر شما چیست؟

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