6.2. استخراج الميزات#

يمكن استخدام وحدة sklearn.feature_extraction لـ استخراج الميزات بتنسيق تدعمه خوارزميات التعلم الآلي من مجموعات البيانات المُؤلفة من تنسيقات مثل النص والصورة.

ملاحظة

يختلف استخراج الميزات تمامًا عن اختيار الميزات: يتمثل الأول في تحويل البيانات التعسفية، مثل النص أو الصور، إلى ميزات رقمية قابلة للاستخدام للتعلم الآلي. الأخير هو تقنية تعلم آلي تُطبق على هذه الميزات.

6.2.1. تحميل الميزات من القواميس#

يمكن استخدام فئة DictVectorizer لتحويل مصفوفات الميزات المُمثلة كقوائم لكائنات Python dict القياسية إلى تمثيل NumPy/SciPy الذي تستخدمه مُقدِّرات scikit-learn.

على الرغم من أنه ليس سريعًا بشكل خاص في المعالجة، إلا أن dict في Python له مزايا كونه سهل الاستخدام، ومتفرق (لا يلزم تخزين الميزات الغائبة) وتخزين أسماء الميزات بالإضافة إلى القيم.

DictVectorizer يُطبق ما يسمى بترميز واحد من K أو "ترميز أحادي ساخن" للميزات الفئوية (المعروفة أيضًا باسم الاسمية، المنفصلة). الميزات الفئوية هي أزواج "سمة-قيمة" حيث تقتصر القيمة على قائمة من الاحتمالات المنفصلة بدون ترتيب (على سبيل المثال مُعرفات الموضوع، أنواع الكائنات، العلامات، الأسماء...).

فيما يلي، "المدينة" هي سمة فئوية بينما "درجة الحرارة" هي ميزة رقمية تقليدية:

>>> measurements = [
...     {'city': 'Dubai', 'temperature': 33.},
...     {'city': 'London', 'temperature': 12.},
...     {'city': 'San Francisco', 'temperature': 18.},
... ]

>>> from sklearn.feature_extraction import DictVectorizer
>>> vec = DictVectorizer()

>>> vec.fit_transform(measurements).toarray()
array([[ 1.,  0.,  0., 33.],
       [ 0.,  1.,  0., 12.],
       [ 0.,  0.,  1., 18.]])

>>> vec.get_feature_names_out()
array(['city=Dubai', 'city=London', 'city=San Francisco', 'temperature'], ...)

DictVectorizer يقبل قيم سلسلة متعددة لميزة واحدة، مثل، على سبيل المثال، فئات متعددة لفيلم.

افترض أن قاعدة بيانات تُصنف كل فيلم باستخدام بعض الفئات (غير إلزامية) وعام إصداره.

>>> movie_entry = [{'category': ['thriller', 'drama'], 'year': 2003},
...                {'category': ['animation', 'family'], 'year': 2011},
...                {'year': 1974}]
>>> vec.fit_transform(movie_entry).toarray()
array([[0.000e+00, 1.000e+00, 0.000e+00, 1.000e+00, 2.003e+03],
       [1.000e+00, 0.000e+00, 1.000e+00, 0.000e+00, 2.011e+03],
       [0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00, 1.974e+03]])
>>> vec.get_feature_names_out()
array(['category=animation', 'category=drama', 'category=family',
       'category=thriller', 'year'], ...)
>>> vec.transform({'category': ['thriller'],
...                'unseen_feature': '3'}).toarray()
array([[0., 0., 0., 1., 0.]])

DictVectorizer هو أيضًا تحويل تمثيل مفيد لـ تدريب مُصنِّفات التسلسل في نماذج مُعالجة اللغة الطبيعية التي تعمل عادةً عن طريق استخراج نوافذ الميزات حول كلمة مُعينة ذات أهمية.

على سبيل المثال، لنفترض أن لدينا خوارزمية أولى تستخرج علامات جزء من الكلام (PoS) التي نُريد استخدامها كعلامات تكميلية لـ تدريب مُصنف تسلسل (على سبيل المثال قطَّاعة). يمكن أن يكون القاموس التالي مثل نافذة من الميزات المستخرجة حول كلمة "sat" في الجملة "The cat sat on the mat.":

>>> pos_window = [
...     {
...         'word-2': 'the',
...         'pos-2': 'DT',
...         'word-1': 'cat',
...         'pos-1': 'NN',
...         'word+1': 'on',
...         'pos+1': 'PP',
...     },
...     # في تطبيق حقيقي، سيستخرج المرء العديد من هذه القواميس
... ]

يمكن تحويل هذا الوصف إلى مصفوفة ثنائية الأبعاد متفرقة مُناسبة لتغذية مُصنف (ربما بعد تمريره إلى TfidfTransformer للتطبيع):

>>> vec = DictVectorizer()
>>> pos_vectorized = vec.fit_transform(pos_window)
>>> pos_vectorized
<Compressed Sparse...dtype 'float64'
  with 6 stored elements and shape (1, 6)>
>>> pos_vectorized.toarray()
array([[1., 1., 1., 1., 1., 1.]])
>>> vec.get_feature_names_out()
array(['pos+1=PP', 'pos-1=NN', 'pos-2=DT', 'word+1=on', 'word-1=cat',
       'word-2=the'], ...)

كما تتخيل، إذا استخرج المرء مثل هذا السياق حول كل كلمة من مجموعة من المستندات، فستكون المصفوفة الناتجة واسعة جدًا (العديد من الميزات أحادية التشفير الساخن) مع كون معظمها بقيمة صفر في معظم الأوقات. من أجل جعل بنية البيانات الناتجة قادرة على احتواء الذاكرة، تستخدم فئة DictVectorizer مصفوفة scipy.sparse افتراضيًا بدلاً من numpy.ndarray.

6.2.2. تجزئة الميزات#

فئة FeatureHasher هي أداة متجهية عالية السرعة ومنخفضة الذاكرة تستخدم تقنية تُعرف باسم تجزئة الميزات، أو "خدعة التجزئة". بدلاً من بناء جدول تجزئة للميزات التي تمت مواجهتها في التدريب، كما تفعل أدوات المتجهات، تقوم مثيلات FeatureHasher بتطبيق دالة تجزئة على الميزات لتحديد فهرس عمودها في مصفوفات العينات مباشرةً. النتيجة هي زيادة السرعة وتقليل استخدام الذاكرة، على حساب إمكانية الفحص؛ لا تتذكر أداة التجزئة شكل ميزات الإدخال وليس لديها أسلوب inverse_transform.

نظرًا لأن دالة التجزئة قد تُسبب تصادمات بين الميزات (غير ذات الصلة)، يتم استخدام دالة تجزئة مُوقعة ويُحدد إشارة قيمة التجزئة إشارة القيمة المخزنة في مصفوفة الإخراج لميزة. بهذه الطريقة، من المرجح أن تُلغي التصادمات بعضها البعض بدلاً من تراكم الخطأ، والمتوسط المتوقع لقيمة أي ميزة إخراج هو صفر. يتم تمكين هذه الآلية افتراضيًا باستخدام alternate_sign=True وهي مفيدة بشكل خاص لأحجام جداول التجزئة الصغيرة (n_features < 10000). بالنسبة لأحجام جداول التجزئة الكبيرة، يمكن تعطيلها، للسماح بتمرير الإخراج إلى مُقدِّرات مثل MultinomialNB أو مُحددات الميزات chi2 التي تتوقع مدخلات غير سالبة.

FeatureHasher يقبل إما تعيينات (مثل Python dict ومتغيراتها في وحدة collections)، أزواج (ميزة، قيمة)، أو سلاسل، اعتمادًا على معلمة المُنشئ input_type. يتم مُعالجة التعيينات كقوائم من أزواج (ميزة، قيمة)، بينما تحتوي السلاسل المفردة على قيمة ضمنية تساوي 1، لذا يتم تفسير ['feat1'، 'feat2'، 'feat3'] على أنها [('feat1'، 1)، ('feat2'، 1)، ('feat3'، 1)]. إذا ظهرت ميزة واحدة عدة مرات في عينة، فسيتم جمع القيم المرتبطة (لذا يصبح ('feat'، 2) و ('feat'، 3.5) ('feat'، 5.5)). يكون إخراج FeatureHasher دائمًا مصفوفة scipy.sparse بتنسيق CSR.

يمكن استخدام تجزئة الميزات في تصنيف المستندات، ولكن على عكس CountVectorizer، FeatureHasher لا يقوم بتقسيم الكلمات أو أي مُعالجة مُسبقة أخرى باستثناء ترميز Unicode إلى UTF-8؛ انظر تحويل مجموعة نصية كبيرة إلى متجهات باستخدام خدعة التجزئة، أدناه، لـ مُجزئ / أداة تجزئة مُجمَّعة.

على سبيل المثال، ضع في اعتبارك مهمة مُعالجة لغة طبيعية على مستوى الكلمة تحتاج إلى ميزات مستخرجة من أزواج (رمز مميز، جزء من الكلام). يمكن للمرء استخدام دالة مُولِّد Python لاستخراج الميزات:

def token_features(token, part_of_speech):
    if token.isdigit():
        yield "numeric"
    else:
        yield "token={}".format(token.lower())
        yield "token,pos={},{}".format(token, part_of_speech)
    if token[0].isupper():
        yield "uppercase_initial"
    if token.isupper():
        yield "all_uppercase"
    yield "pos={}".format(part_of_speech)

ثم، يمكن إنشاء raw_X ليتم تغذيته إلى FeatureHasher.transform باستخدام:

raw_X = (token_features(tok, pos_tagger(tok)) for tok in corpus)

ويتم تغذيته إلى أداة تجزئة باستخدام:

hasher = FeatureHasher(input_type='string')
X = hasher.transform(raw_X)

للحصول على مصفوفة scipy.sparse X.

لاحظ استخدام فهم المُولِّد، الذي يُقدم الكسل في استخراج الميزات: لا تتم مُعالجة الرموز المميزة إلا عند الطلب من أداة التجزئة.

تفاصيل التطبيق#

FeatureHasher يستخدم متغير MurmurHash3 المُوقع 32 بت. كنتيجة (وبسبب القيود في scipy.sparse)، الحد الأقصى لعدد الميزات المدعومة حاليًا هو \(2^{31} - 1\).

الصيغة الأصلية لخدعة التجزئة من قِبل Weinberger وآخرون. استخدمت دالتي تجزئة مُنفصلتين \(h\) و \(\xi\) لتحديد فهرس العمود وإشارة الميزة، على التوالي. يعمل التطبيق الحالي على افتراض أن بت الإشارة لـ MurmurHash3 مستقل عن البتات الأخرى.

نظرًا لاستخدام معيار بسيط لتحويل دالة التجزئة إلى فهرس عمود، فمن المستحسن استخدام أس اثنين كمعلمة n_features؛ وإلا فلن يتم تعيين الميزات بالتساوي للأعمدة.

المراجع

المراجع

6.2.3. استخراج ميزات النص#

6.2.3.1. تمثيل حقيبة الكلمات#

يُعد تحليل النص مجال تطبيق رئيسي لخوارزميات التعلم الآلي. ومع ذلك، لا يمكن تغذية البيانات الأولية، وهي تسلسل من الرموز، مباشرةً إلى الخوارزميات نفسها حيث أن معظمها يتوقع متجهات ميزات رقمية ذات حجم ثابت بدلاً من مستندات النص الأولية ذات الطول المُتغير.

من أجل مُعالجة ذلك، يُوفر scikit-learn أدوات مساعدة لـ الطرقالأكثر شيوعًا لاستخراج الميزات الرقمية من محتوى النص، وهي:

  • تجزئة السلاسل وإعطاء مُعرف عدد صحيح لكل رمز مميز مُمكن، على سبيل المثال باستخدام المسافات البيضاء وعلامات الترقيم كفواصل للرموز المميزة.

  • حساب تكرارات الرموز المميزة في كل مستند.

  • تطبيع ووزن الرموز المميزة ذات الأهمية المتناقصة التي تظهر في غالبية العينات / المستندات.

في هذا المخطط، يتم تعريف الميزات والعينات على النحو التالي:

  • يتم مُعالجة كل تردد تكرار رمز مميز فردي (مُقيَّس أو غير مُقيَّس) كميزة.

  • يُعتبر متجه جميع ترددات الرموز المميزة لـ مستند مُعين عينة متعددة المتغيرات.

وبالتالي، يمكن تمثيل مجموعة من المستندات بواسطة مصفوفة تحتوي على صف واحد لكل مستند وعمود واحد لكل رمز مميز (على سبيل المثال كلمة) يظهر في المجموعة.

نُسمي التحويل إلى متجهات العملية العامة لتحويل مجموعة من مستندات النص إلى متجهات ميزات رقمية. تُسمى هذه الإستراتيجية المحددة (التجزئة والعد والتطبيع) حقيبة الكلمات أو تمثيل "حقيبة n-grams". يتم وصف المستندات بواسطة تكرارات الكلمات مع تجاهل معلومات الموضع النسبي للكلمات في المستند تمامًا.

6.2.3.2. التفرق#

نظرًا لأن معظم المستندات ستستخدم عادةً مجموعة فرعية صغيرة جدًا من الكلمات المُستخدمة في المجموعة، فستحتوي المصفوفة الناتجة على العديد من قيم الميزات التي تساوي صفرًا (عادةً أكثر من 99% منها).

على سبيل المثال، ستستخدم مجموعة من 10000 مستند نصي قصير (مثل رسائل البريد الإلكتروني) مفردات بحجم يبلغ حوالي 100000 كلمة فريدة إجمالاً بينما سيستخدم كل مستند من 100 إلى 1000 كلمة فريدة على حدة.

من أجل أن تكون قادرًا على تخزين مثل هذه المصفوفة في الذاكرة ولكن أيضًا لتسريع العمليات الجبرية للمصفوفة / المتجه، ستستخدم التطبيقات عادةً تمثيلًا متفرقًا مثل التطبيقات المتاحة في حزمة scipy.sparse.

6.2.3.3. استخدام متجه شائع#

CountVectorizer يُطبق كل من التجزئة و حساب التكرار في فئة واحدة:

>>> from sklearn.feature_extraction.text import CountVectorizer

يحتوي هذا النموذج على العديد من المعلمات، ومع ذلك، فإن القيم الافتراضية معقولة تمامًا (يرجى مراجعة وثائق المرجع للتفاصيل):

>>> vectorizer = CountVectorizer()
>>> vectorizer
CountVectorizer()

دعونا نستخدمه لتجزئة وحساب تكرارات الكلمات لمجموعة بيانات نصية بسيطة:

>>> corpus = [
...     'This is the first document.',
...     'This is the second second document.',
...     'And the third one.',
...     'Is this the first document?',
... ]
>>> X = vectorizer.fit_transform(corpus)
>>> X
<Compressed Sparse...dtype 'int64'
  with 19 stored elements and shape (4, 9)>

يقوم التكوين الافتراضي بتجزئة السلسلة عن طريق استخراج الكلمات التي تحتوي على حرفين على الأقل. يمكن طلب الدالة المحددة التي تُجري هذه الخطوة صراحةً:

>>> analyze = vectorizer.build_analyzer()
>>> analyze("This is a text document to analyze.") == (
...     ['this', 'is', 'text', 'document', 'to', 'analyze'])
True

يتم تعيين فهرس عدد صحيح فريد لكل مُصطلح تم العثور عليه بواسطة المُحلل أثناء الملاءمة، مُقابل عمود في المصفوفة الناتجة. هذا التفسير للأعمدة يمكن استرداده على النحو التالي:

>>> vectorizer.get_feature_names_out()
array(['and', 'document', 'first', 'is', 'one', 'second', 'the',
       'third', 'this'], ...)

>>> X.toarray()
array([[0, 1, 1, 1, 0, 0, 1, 0, 1],
       [0, 1, 0, 1, 0, 2, 1, 0, 1],
       [1, 0, 0, 0, 1, 0, 1, 1, 0],
       [0, 1, 1, 1, 0, 0, 1, 0, 1]]...)

يتم تخزين التعيين العكسي من اسم الميزة إلى فهرس العمود في سمة vocabulary_ لـ vectorizer:

>>> vectorizer.vocabulary_.get('document')
1

وبالتالي، سيتم تجاهل الكلمات التي لم تتم رؤيتها في مجموعة التدريب تمامًا في الاستدعاءات المستقبلية لأسلوب التحويل:

>>> vectorizer.transform(['Something completely new.']).toarray()
array([[0, 0, 0, 0, 0, 0, 0, 0, 0]]...)

لاحظ أنه في المجموعة السابقة، يحتوي المستند الأول والأخير على نفس الكلمات تمامًا، وبالتالي يتم ترميزها في متجهات متساوية. على وجه الخصوص نفقد المعلومات التي تفيد بأن المستند الأخير هو شكل استفهام. لـ الحفاظ على بعض معلومات ترتيب المواقع، يمكننا استخراج 2-grams من الكلمات بالإضافة إلى 1-grams (الكلمات الفردية):

>>> bigram_vectorizer = CountVectorizer(ngram_range=(1, 2),
...                                     token_pattern=r'\b\w+\b', min_df=1)
>>> analyze = bigram_vectorizer.build_analyzer()
>>> analyze('Bi-grams are cool!') == (
...     ['bi', 'grams', 'are', 'cool', 'bi grams', 'grams are', 'are cool'])
True

وبالتالي، فإن المفردات التي استخرجها هذا المتجه أكبر بكثير و يمكنها الآن حل الغموض المُرمَّز في أنماط تحديد المواقع:

>>> X_2 = bigram_vectorizer.fit_transform(corpus).toarray()
>>> X_2
array([[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0],
       [0, 0, 1, 0, 0, 1, 1, 0, 0, 2, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0],
       [1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0],
       [0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1]]...)

على وجه الخصوص، شكل الاستفهام "Is this" موجود فقط في المستند الأخير:

>>> feature_index = bigram_vectorizer.vocabulary_.get('is this')
>>> X_2[:, feature_index]
array([0, 0, 0, 1]...)

6.2.3.4. استخدام كلمات الإيقاف#

كلمات الإيقاف هي كلمات مثل "و" و "ال" و "هو"، والتي يُفترض أنها غير مفيدة في تمثيل محتوى النص، والتي يمكن إزالتها لتجنب تفسيرها على أنها إشارة للتنبؤ. ومع ذلك، في بعض الأحيان، تكون الكلمات المُشابهة مفيدة للتنبؤ، كما هو الحال في تصنيف أسلوب الكتابة أو الشخصية.

هناك العديد من المشاكل المعروفة في قائمة كلمات الإيقاف "الإنجليزية" التي نُقدمها. إنها لا تهدف إلى أن تكون حلاً عامًا "يناسب الجميع" حيث أن بعض المهام قد تتطلب حلاً أكثر تخصيصًا. انظر [NQY18] لمزيد من التفاصيل.

يرجى توخي الحذر عند اختيار قائمة كلمات الإيقاف. قد تتضمن قوائم كلمات الإيقاف الشائعة كلمات مفيدة للغاية لبعض المهام، مثل الكمبيوتر.

يجب عليك أيضًا التأكد من أن قائمة كلمات الإيقاف قد خضعت لنفس المعالجة المسبقة والتجزئة المُستخدمة في أداة المتجه. يتم تقسيم كلمة we've إلى we و ve بواسطة مُجزئ CountVectorizer الافتراضي، لذلك إذا كانت we've في stop_words، ولكن ve ليست كذلك، فسيتم الاحتفاظ بـ ve من we've في النص المُحوَّل. ستحاول أدوات المتجهات الخاصة بنا تحديد والتحذير من بعض أنواع عدم الاتساق.

المراجع

[NQY18]

J. Nothman, H. Qin and R. Yurchak (2018). "قوائم كلمات الإيقاف في حزم البرامج مفتوحة المصدر المجانية". في وقائع ورشة عمل لبرامج NLP مفتوحة المصدر.

6.2.3.5. وزن مُصطلح Tf-idf#

في مجموعة نصية كبيرة، ستكون بعض الكلمات موجودة جدًا (على سبيل المثال "the" و "a" و "is" باللغة الإنجليزية)، وبالتالي تحمل معلومات قليلة جدًا ذات مغزى حول المحتويات الفعلية للمستند. إذا كنا سنغذي بيانات العد المباشر مباشرةً إلى مُصنف، فإن هذه المصطلحات المتكررة جدًا ستُلقي بظلالها على ترددات المصطلحات الأكثر ندرة والأكثر إثارة للاهتمام.

من أجل إعادة وزن ميزات العد إلى قيم النقطة العائمة المُناسبة للاستخدام بواسطة مُصنف، من الشائع جدًا استخدام تحويل tf-idf.

Tf تعني تردد المصطلح بينما tf-idf تعني تردد المصطلح مضروبًا في تردد المستند العكسي: \(\text{tf-idf(t,d)}=\text{tf(t,d)} \times \text{idf(t)}\).

باستخدام الإعدادات الافتراضية لـ TfidfTransformer، TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False) يتم ضرب تردد المصطلح، عدد مرات ظهور مصطلح في مستند مُعين، في مكون idf، والذي يتم حسابه على النحو التالي

\(\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1\)،

حيث \(n\) هو العدد الإجمالي للمستندات في مجموعة المستندات، و \(\text{df}(t)\) هو عدد المستندات في مجموعة المستندات التي تحتوي على المصطلح \(t\). ثم يتم تطبيع متجهات tf-idf الناتجة بواسطة قاعدة إقليدية:

\(v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + v{_2}^2 + \dots + v{_n}^2}}\).

كان هذا في الأصل مخططًا لوزن المصطلحات تم تطويره لاسترجاع المعلومات (كدالة تصنيف لنتائج محركات البحث) والذي وجد أيضًا استخدامًا جيدًا في تصنيف المستندات والتجميع.

تحتوي الأقسام التالية على مزيد من الشروحات والأمثلة التي توضح كيفية حساب tf-idfs بالضبط وكيف يختلف tf-idfs المحسوب في TfidfTransformer و TfidfVectorizer في scikit-learn قليلاً عن تدوين الكتاب المدرسي القياسي الذي يُعرِّف idf على النحو التالي

\(\text{idf}(t) = \log{\frac{n}{1+\text{df}(t)}}\).

في TfidfTransformer و TfidfVectorizer مع smooth_idf=False، يتم إضافة العدد "1" إلى idf بدلاً من مقام idf:

\(\text{idf}(t) = \log{\frac{n}{\text{df}(t)}} + 1\)

يتم تنفيذ هذا التطبيع بواسطة فئة TfidfTransformer:

>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> transformer = TfidfTransformer(smooth_idf=False)
>>> transformer
TfidfTransformer(smooth_idf=False)

مرة أخرى، يرجى مراجعة وثائق المرجع للحصول على تفاصيل حول جميع المعلمات.

مثال رقمي على مصفوفة tf-idf#

لنأخذ مثالاً مع الأعداد التالية. المصطلح الأول موجود 100% من الوقت، وبالتالي فهو غير مثير للاهتمام للغاية. الميزتان الأخريان موجودتان فقط في أقل من 50% من الوقت، وبالتالي من المحتمل أن تكونا أكثر تمثيلًا لـ محتوى المستندات:

>>> counts = [[3, 0, 1],
...           [2, 0, 0],
...           [3, 0, 0],
...           [4, 0, 0],
...           [3, 2, 0],
...           [3, 0, 2]]
...
>>> tfidf = transformer.fit_transform(counts)
>>> tfidf
<Compressed Sparse...dtype 'float64'
  with 9 stored elements and shape (6, 3)>

>>> tfidf.toarray()
array([[0.81940995, 0.        , 0.57320793],
      [1.        , 0.        , 0.        ],
      [1.        , 0.        , 0.        ],
      [1.        , 0.        , 0.        ],
      [0.47330339, 0.88089948, 0.        ],
      [0.58149261, 0.        , 0.81355169]])

يتم تطبيع كل صف ليكون له قاعدة إقليدية للوحدة:

\(v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + v{_2}^2 + \dots + v{_n}^2}}\)

على سبيل المثال، يمكننا حساب tf-idf للمصطلح الأول في المستند الأول في مصفوفة counts على النحو التالي:

\(n = 6\)

\(\text{df}(t)_{\text{term1}} = 6\)

\(\text{idf}(t)_{\text{term1}} = \log \frac{n}{\text{df}(t)} + 1 = \log(1)+1 = 1\)

\(\text{tf-idf}_{\text{term1}} = \text{tf} \times \text{idf} = 3 \times 1 = 3\)

الآن، إذا كررنا هذا الحساب للمصطلحين المتبقيين في المستند، فإننا نحصل على

\(\text{tf-idf}_{\text{term2}} = 0 \times (\log(6/1)+1) = 0\)

\(\text{tf-idf}_{\text{term3}} = 1 \times (\log(6/2)+1) \approx 2.0986\)

ومتجه tf-idfs الأولي:

\(\text{tf-idf}_{\text{raw}} = [3, 0, 2.0986].\)

ثم، بتطبيق قاعدة إقليدية (L2)، نحصل على tf-idfs التالي للمستند 1:

\(\frac{[3, 0, 2.0986]}{\sqrt{\big(3^2 + 0^2 + 2.0986^2\big)}} = [ 0.819, 0, 0.573].\)

علاوة على ذلك، تُضيف المعلمة الافتراضية smooth_idf=True "1" إلى البسط و المقام كما لو تمت رؤية مستند إضافي يحتوي على كل مصطلح في المجموعة مرة واحدة بالضبط، مما يمنع القسمة على الصفر:

\(\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1\)

باستخدام هذا التعديل، يتغير tf-idf للمصطلح الثالث في المستند 1 إلى 1.8473:

\(\text{tf-idf}_{\text{term3}} = 1 \times \log(7/3)+1 \approx 1.8473\)

ويتغير tf-idf المُقيَّس بـ L2 إلى

\(\frac{[3, 0, 1.8473]}{\sqrt{\big(3^2 + 0^2 + 1.8473^2\big)}} = [0.8515, 0, 0.5243]\):

>>> transformer = TfidfTransformer()
>>> transformer.fit_transform(counts).toarray()
array([[0.85151335, 0.        , 0.52433293],
      [1.        , 0.        , 0.        ],
      [1.        , 0.        , 0.        ],
      [1.        , 0.        , 0.        ],
      [0.55422893, 0.83236428, 0.        ],
      [0.63035731, 0.        , 0.77630514]])

يتم تخزين أوزان كل ميزة محسوبة بواسطة استدعاء أسلوب fit في سمة النموذج:

>>> transformer.idf_
array([1. ..., 2.25..., 1.84...])

نظرًا لأن tf-idf غالبًا ما يُستخدم لميزات النص، فهناك أيضًا فئة أخرى تسمى TfidfVectorizer تجمع بين جميع خيارات CountVectorizer و TfidfTransformer في نموذج واحد:

>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> vectorizer = TfidfVectorizer()
>>> vectorizer.fit_transform(corpus)
<Compressed Sparse...dtype 'float64'
  with 19 stored elements and shape (4, 9)>

بينما يكون تطبيع tf-idf مفيدًا جدًا في كثير من الأحيان، قد تكون هناك حالات يكون فيها علامات التواجد الثنائية أفضل ميزات. يمكن تحقيق ذلك باستخدام معلمة binary لـ CountVectorizer. على وجه الخصوص، بعض المُقدِّرات مثل خوارزمية بايز الساذجة متعددة الحدود تُنمذج صراحةً متغيرات عشوائية منطقية منفصلة. أيضًا، من المرجح أن تحتوي النصوص القصيرة جدًا على قيم tf-idf صاخبة بينما تكون معلومات التواجد الثنائية أكثر استقرارًا.

كالعادة، أفضل طريقة لضبط معلمات استخراج الميزات هي استخدام بحث شبكي مُتحقق منه بشكل متبادل، على سبيل المثال عن طريق توصيل مستخرج الميزات مع مُصنف:

6.2.3.6. فك تشفير ملفات النص#

يتكون النص من أحرف، لكن الملفات تتكون من بايتات. تُمثِّل هذه البايتات الأحرف وفقًا لـ ترميز مُعين. للعمل مع ملفات النص في Python، يجب فك تشفير بايتاتها إلى مجموعة أحرف تُسمى Unicode. الترميزات الشائعة هي ASCII و Latin-1 (أوروبا الغربية) و KOI8-R (الروسية) والترميزات العالمية UTF-8 و UTF-16. يوجد العديد من الترميزات الأخرى.

ملاحظة

يمكن أيضًا تسمية الترميز "مجموعة أحرف"، لكن هذا المصطلح أقل دقة: يمكن أن توجد عدة ترميزات لمجموعة أحرف واحدة.

تعرف أدوات استخراج ميزات النص في scikit-learn كيفية فك تشفير ملفات النص، ولكن فقط إذا أخبرتها بالترميز الذي تستخدمه الملفات. يأخذ CountVectorizer معلمة encoding لهذا الغرض. بالنسبة لملفات النص الحديثة، ربما يكون الترميز الصحيح هو UTF-8، وهو بالتالي الإعداد الافتراضي (encoding="utf-8").

إذا كان النص الذي تُحمِّله ليس مُرمَّزًا بالفعل باستخدام UTF-8، فستحصل على UnicodeDecodeError. يمكن إخبار أدوات المتجهات بالتزام الصمت بشأن أخطاء فك التشفير عن طريق تعيين معلمة decode_error إما إلى "ignore" أو "replace". راجع وثائق دالة Python bytes.decode لمزيد من التفاصيل (اكتب help(bytes.decode) في موجه Python).

استكشاف أخطاء فك تشفير النص وإصلاحها#

إذا كنت تُواجه مشكلة في فك تشفير النص، فإليك بعض الأشياء التي يمكنك تجربتها:

  • اكتشف ما هو الترميز الفعلي للنص. قد يأتي الملف مع رأس أو ملف README يُخبرك بالترميز، أو قد يكون هناك بعض الترميز القياسي الذي يمكنك افتراضه بناءً على مصدر النص.

  • قد تتمكن من معرفة نوع الترميز بشكل عام باستخدام أمر UNIX file. تأتي وحدة Python chardet مع نص برمجي يسمى chardetect.py سيخمن الترميز المحدد، على الرغم من أنه لا يمكنك الاعتماد على أن تخمينه صحيح.

  • يمكنك تجربة UTF-8 وتجاهل الأخطاء. يمكنك فك تشفير سلاسل البايت باستخدام bytes.decode(errors='replace') لاستبدال جميع أخطاء فك التشفير بحرف لا معنى له، أو تعيين decode_error='replace' في أداة المتجه. قد يؤدي هذا إلى إتلاف فائدة ميزاتك.

  • قد يأتي النص الحقيقي من مجموعة متنوعة من المصادر التي قد تستخدم ترميزات مختلفة، أو حتى يتم فك تشفيرها بشكل مُهمل بترميز مختلف عن الترميز الذي تم ترميزها به. هذا شائع في النص الذي تم استرداده من الويب. حزمة Python ftfy يمكنها فرز بعض فئات أخطاء فك التشفير تلقائيًا، لذلك يمكنك تجربة فك تشفير النص غير المعروف كـ latin-1 ثم استخدام ftfy لإصلاح الأخطاء.

  • إذا كان النص عبارة عن خليط من الترميزات يصعب فرزها (كما هو الحال بالنسبة لمجموعة بيانات 20 مجموعة إخبارية)، يمكنك الرجوع إلى ترميز بايت واحد بسيط مثل latin-1. قد يتم عرض بعض النصوص بشكل غير صحيح، ولكن على الأقل سيمثل نفس تسلسل البايت دائمًا نفس الميزة.

على سبيل المثال، يستخدم المقتطف التالي chardet (لا يتم شحنه مع scikit-learn، يجب تثبيته بشكل مُنفصل) لتحديد ترميز ثلاثة نصوص. ثم يُحوِّل النصوص إلى متجهات ويطبع المفردات التي تم تعلمها. لم يتم عرض الإخراج هنا.

>>> import chardet    
>>> text1 = b"Sei mir gegr\xc3\xbc\xc3\x9ft mein Sauerkraut"
>>> text2 = b"holdselig sind deine Ger\xfcche"
>>> text3 = b"\xff\xfeA\x00u\x00f\x00 \x00F\x00l\x00\xfc\x00g\x00e\x00l\x00n\x00 \x00d\x00e\x00s\x00 \x00G\x00e\x00s\x00a\x00n\x00g\x00e\x00s\x00,\x00 \x00H\x00e\x00r\x00z\x00l\x00i\x00e\x00b\x00c\x00h\x00e\x00n\x00,\x00 \x00t\x00r\x00a\x00g\x00 \x00i\x00c\x00h\x00 \x00d\x00i\x00c\x00h\x00 \x00f\x00o\x00r\x00t\x00"
>>> decoded = [x.decode(chardet.detect(x)['encoding'])
...            for x in (text1, text2, text3)]        
>>> v = CountVectorizer().fit(decoded).vocabulary_    
>>> for term in v: print(v)                           

(اعتمادًا على إصدار chardet، قد يُخطئ في الأول.)

للحصول على مقدمة عن Unicode وترميزات الأحرف بشكل عام، انظر الحد الأدنى المُطلق الذي يجب أن يعرفه كل مُطور برامج حول Unicode لـ Joel Spolsky.

6.2.3.7. التطبيقات والأمثلة#

تمثيل حقيبة الكلمات مُبسط تمامًا ولكنه مفيد بشكل مُثير للدهشة في الممارسة العملية.

على وجه الخصوص في إعداد خاضع للإشراف، يمكن دمجه بنجاح مع النماذج الخطية السريعة والقابلة للتطوير لتدريب مُصنِّفات المستندات، على سبيل المثال:

في إعداد غير خاضع للإشراف، يمكن استخدامه لتجميع المستندات المُشابهة معًا عن طريق تطبيق خوارزميات التجميع مثل K-means:

أخيرًا، من الممكن اكتشاف الموضوعات الرئيسية لمجموعة بيانات عن طريق تخفيف قيود التعيين الصعبة للتجميع، على سبيل المثال باستخدام تحليل المصفوفة غير السالبة (NMF أو NNMF):

  • sphx_glr_auto_examples_applications_plot_topics_extraction_with_nmf_lda.py

6.2.3.8. قيود تمثيل حقيبة الكلمات#

لا يمكن لمجموعة من unigrams (ما هي حقيبة الكلمات) التقاط العبارات والتعبيرات متعددة الكلمات، مع تجاهل أي تبعية لترتيب الكلمات بشكل فعال. بالإضافة إلى ذلك، لا يُراعي نموذج حقيبة الكلمات الأخطاء الإملائية المُحتملة أو اشتقاقات الكلمات.

N-grams للإنقاذ! بدلاً من بناء مجموعة بسيطة من unigrams (n = 1)، قد يُفضل المرء مجموعة من bigrams (n = 2)، حيث يتم حساب تكرارات أزواج الكلمات المتتالية.

قد يفكر المرء بدلاً من ذلك في مجموعة من n-grams للأحرف، وهو تمثيل مرن ضد الأخطاء الإملائية والاشتقاقات.

على سبيل المثال، لنفترض أننا نتعامل مع مجموعة من مستندين: ['words'، 'wprds']. يحتوي المستند الثاني على خطأ إملائي لكلمة "words". سيمثل تمثيل حقيبة كلمات بسيط هذين كمستندين مُختلفين للغاية، يختلفان في كل من الميزتين المُمكنتين. ومع ذلك، سيجد تمثيل حرف 2-gram تطابقًا بين المستندين في 4 من 8 ميزات، مما قد يُساعد المُصنف المُفضل على اتخاذ قرار أفضل:

>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(2, 2))
>>> counts = ngram_vectorizer.fit_transform(['words', 'wprds'])
>>> ngram_vectorizer.get_feature_names_out()
array([' w', 'ds', 'or', 'pr', 'rd', 's ', 'wo', 'wp'], ...)
>>> counts.toarray().astype(int)
array([[1, 1, 1, 0, 1, 1, 1, 0],
       [1, 1, 0, 1, 1, 1, 0, 1]])

في المثال أعلاه، يتم استخدام مُحلل char_wb، الذي يُنشئ n-grams فقط من الأحرف داخل حدود الكلمات (مُبطَّنة بمسافة على كل جانب). مُحلل char، بدلاً من ذلك، يُنشئ n-grams التي تمتد عبر الكلمات:

>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
<Compressed Sparse...dtype 'int64'
  with 4 stored elements and shape (1, 4)>

>>> ngram_vectorizer.get_feature_names_out()
array([' fox ', ' jump', 'jumpy', 'umpy '], ...)

>>> ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
<Compressed Sparse...dtype 'int64'
  with 5 stored elements and shape (1, 5)>
>>> ngram_vectorizer.get_feature_names_out()
array(['jumpy', 'mpy f', 'py fo', 'umpy ', 'y fox'], ...)

يُعد المتغير char_wb المُدرك لحدود الكلمات مثيرًا للاهتمام بشكل خاص للغات التي تستخدم مسافات بيضاء لفصل الكلمات لأنه يُولِّد ميزات أقل ضوضاء بشكل ملحوظ من متغير char الأولي في تلك الحالة. بالنسبة لهذه اللغات، يمكن أن يزيد من كل من دقة التنبؤ وسرعة تقارب المُصنِّفات المُدرَّبة باستخدام هذه الميزات مع الحفاظ على المتانة فيما يتعلق بالأخطاء الإملائية و اشتقاقات الكلمات.

بينما يمكن الحفاظ على بعض معلومات تحديد المواقع عن طريق استخراج n-grams بدلاً من الكلمات الفردية، فإن حقيبة الكلمات وحقيبة n-grams تُدمر معظم البنية الداخلية للمستند وبالتالي معظم المعنى الذي تحمله تلك البنية الداخلية.

من أجل مُعالجة المهمة الأوسع لفهم اللغة الطبيعية، يجب مراعاة الهيكل المحلي للجمل والفقرات. سيتم صياغة العديد من هذه النماذج على أنها مشاكل "إخراج مُهيكلة" والتي تقع حاليًا خارج نطاق scikit-learn.

6.2.3.9. تحويل مجموعة نصية كبيرة إلى متجهات باستخدام خدعة التجزئة#

مخطط التحويل إلى متجهات أعلاه بسيط ولكن حقيقة أنه يحمل تعيينًا في الذاكرة من الرموز المميزة للسلسلة إلى مؤشرات الميزات الصحيحة (سمة vocabulary_) تُسبب العديد من المشاكل عند التعامل مع مجموعات البيانات الكبيرة:

  • كلما كبرت المجموعة النصية، زادت المفردات وبالتالي استخدام الذاكرة أيضًا،

  • تتطلب الملاءمة تخصيص هياكل بيانات وسيطة بحجم يتناسب مع حجم مجموعة البيانات الأصلية.

  • يتطلب بناء تعيين الكلمات تمريرة كاملة على مجموعة البيانات، وبالتالي فإنه ليس من الممكن ملاءمة مُصنِّفات النص بطريقة على الإنترنت تمامًا.

  • يمكن أن يكون تخليل وفك تخليل أدوات المتجهات ذات vocabulary_ كبيرة بطيئًا جدًا (عادةً ما يكون أبطأ بكثير من تخليل / فك تخليل هياكل البيانات المسطحة مثل مصفوفة NumPy من نفس الحجم)،

  • ليس من السهل تقسيم عمل التحويل إلى متجهات إلى مهام فرعية متزامنة حيث يجب أن تكون سمة vocabulary_ حالة مُشتركة مع حاجز مزامنة دقيق الحبيبات: يعتمد التعيين من سلسلة الرمز المميز إلى فهرس الميزة على ترتيب التواجد الأول لكل رمز مميز وبالتالي يجب مُشاركته، مما قد يضر بأداء العمال المتزامنين لدرجة جعلهم أبطأ من المتغير التسلسلي.

من الممكن التغلب على هذه القيود من خلال الجمع بين "خدعة التجزئة" (تجزئة الميزات) التي تُطبقها فئة FeatureHasher وميزات المعالجة المسبقة للنص والتجزئة لـ CountVectorizer.

يتم تطبيق هذه المجموعة في HashingVectorizer، وهي فئة مُحوِّل متوافقة مع واجهة برمجة التطبيقات في الغالب مع CountVectorizer. HashingVectorizer عديم الحالة، مما يعني أنه لا يتعين عليك استدعاء fit عليه:

>>> from sklearn.feature_extraction.text import HashingVectorizer
>>> hv = HashingVectorizer(n_features=10)
>>> hv.transform(corpus)
<Compressed Sparse...dtype 'float64'
  with 16 stored elements and shape (4, 10)>

يمكنك أن ترى أنه تم استخراج 16 رمزًا مميزًا غير صفري في متجه الإخراج: هذا أقل من 19 رمزًا مميزًا غير صفري تم استخراجها سابقًا بواسطة CountVectorizer على نفس مجموعة البيانات التوضيحية. يأتي التناقض من تصادمات دالة التجزئة بسبب انخفاض قيمة معلمة n_features.

في إعداد العالم الحقيقي، يمكن ترك معلمة n_features إلى قيمتها الافتراضية 2 ** 20 (حوالي مليون ميزة مُمكنة). إذا كانت الذاكرة أو حجم النماذج في اتجاه مجرى النهر مشكلة، فإن تحديد قيمة أقل مثل 2 ** 18 قد يُساعد دون إدخال الكثير من التصادمات الإضافية على مهام تصنيف النص النموذجية.

لاحظ أن الأبعاد لا تؤثر على وقت تدريب وحدة المعالجة المركزية لـ الخوارزميات التي تعمل على مصفوفات CSR (LinearSVC(dual=True) و Perceptron و SGDClassifier و PassiveAggressive) ولكنها تؤثر على الخوارزميات التي تعمل مع مصفوفات CSC (LinearSVC(dual=False) و Lasso() إلخ.).

دعونا نحاول مرة أخرى مع الإعداد الافتراضي:

>>> hv = HashingVectorizer()
>>> hv.transform(corpus)
<Compressed Sparse...dtype 'float64'
  with 19 stored elements and shape (4, 1048576)>

لم نعد نحصل على التصادمات، لكن هذا يأتي على حساب أبعاد أكبر بكثير لمساحة الإخراج. بالطبع، قد لا تزال المصطلحات الأخرى غير 19 المُستخدمة هنا تصطدم مع بعضها البعض.

يأتي HashingVectorizer أيضًا مع القيود التالية:

  • ليس من الممكن عكس النموذج (لا يوجد أسلوب inverse_transform)، ولا الوصول إلى تمثيل السلسلة الأصلي للميزات، بسبب طبيعة دالة التجزئة أحادية الاتجاه التي تُجري التعيين.

  • لا يُوفر وزن IDF لأن ذلك سيُدخل حالة في النموذج. يمكن إلحاق TfidfTransformer به في خط أنابيب إذا لزم الأمر.

إجراء تغيير مقياس خارج النواة باستخدام HashingVectorizer#

تطور مثير للاهتمام لاستخدام HashingVectorizer هو القدرة على إجراء تغيير مقياس `خارج النواة`_. هذا يعني أنه يمكننا التعلم من البيانات التي لا تتناسب مع الذاكرة الرئيسية للكمبيوتر.

إحدى إستراتيجيات تطبيق تغيير المقياس خارج النواة هي دفق البيانات إلى المُقدِّر في دفعات صغيرة. يتم تحويل كل دفعة صغيرة إلى متجهات باستخدام HashingVectorizer لضمان أن مساحة إدخال المُقدِّر لها دائمًا نفس الأبعاد. وبالتالي، فإن مقدار الذاكرة المُستخدمة في أي وقت مُقيَّد بـ حجم دفعة صغيرة. على الرغم من عدم وجود حد لكمية البيانات التي يمكن استيعابها باستخدام هذا النهج، إلا أن وقت التعلم من الناحية العملية غالبًا ما يكون محدودًا بوقت وحدة المعالجة المركزية الذي يُريد المرء إنفاقه على المهمة.

لمثال كامل على تغيير المقياس خارج النواة في مهمة تصنيف النص، انظر Out-of-core classification of text documents.

6.2.3.10. تخصيص فئات أداة المتجه#

من الممكن تخصيص السلوك عن طريق تمرير دالة قابلة للاستدعاء إلى مُنشئ أداة المتجه:

>>> def my_tokenizer(s):
...     return s.split()
...
>>> vectorizer = CountVectorizer(tokenizer=my_tokenizer)
>>> vectorizer.build_analyzer()(u"Some... punctuation!") == (
...     ['some...', 'punctuation!'])
True

على وجه الخصوص، نُسمي:

  • preprocessor: دالة قابلة للاستدعاء تأخذ مستندًا كاملاً كمدخل (كسلسلة واحدة)، وتُعيد إصدارًا مُحوَّلاً مُحتملًا من المستند، لا يزال كسلسلة كاملة. يمكن استخدام هذا لإزالة علامات HTML، و تحويل المستند بأكمله إلى أحرف صغيرة، إلخ.

  • tokenizer: دالة قابلة للاستدعاء تأخذ الإخراج من المُعالِج المُسبق وتقسِّمه إلى رموز مميزة، ثم تُعيد قائمة بهذه.

  • analyzer: دالة قابلة للاستدعاء تحل محل المُعالِج المُسبق والمُجزئ. تستدعي المُحللات الافتراضية جميعها المُعالِج المُسبق والمُجزئ، لكن المُحللات المُخصصة ستتخطى ذلك. يحدث استخراج N-gram وتصفية كلمات الإيقاف على مستوى المُحلل، لذلك قد يضطر المُحلل المُخصص إلى إعادة إنتاج هذه الخطوات.

(قد يتعرف مستخدمو Lucene على هذه الأسماء، ولكن انتبه إلى أن مفاهيم scikit-learn قد لا تُعيَّن واحد لواحد على مفاهيم Lucene.)

لجعل المُعالِج المُسبق والمُجزئ والمُحللات على دراية بمعلمات النموذج، من الممكن الاشتقاق من الفئة وتجاوز أساليب المصنع build_preprocessor و build_tokenizer و build_analyzer بدلاً من تمرير دوال مُخصصة.

نصائح وحيل#
  • إذا تمت مُعالجة المستندات مُسبقًا بواسطة حزمة خارجية، فقم بتخزينها في ملفات (أو سلاسل) مع فصل الرموز المميزة بمسافات بيضاء وتمرير analyzer=str.split

  • التحليل المُتطور على مستوى الرموز المميزة مثل التصريف والتصريف وتقسيم المُركبات والتصفية بناءً على جزء من الكلام، إلخ. غير مُضمنة في قاعدة بيانات scikit-learn، ولكن يمكن إضافتها عن طريق تخصيص إما المُجزئ أو المُحلل. فيما يلي CountVectorizer مع مُجزئ وأداة تصريف باستخدام NLTK:

    >>> from nltk import word_tokenize          
    >>> from nltk.stem import WordNetLemmatizer 
    >>> class LemmaTokenizer:
    ...     def __init__(self):
    ...         self.wnl = WordNetLemmatizer()
    ...     def __call__(self, doc):
    ...         return [self.wnl.lemmatize(t) for t in word_tokenize(doc)]
    ...
    >>> vect = CountVectorizer(tokenizer=LemmaTokenizer())  
    

    (لاحظ أن هذا لن يُصفِّي علامات الترقيم.)

    سيُحوِّل المثال التالي، على سبيل المثال، بعض التهجئة البريطانية إلى تهجئة أمريكية:

    >>> import re
    >>> def to_british(tokens):
    ...     for t in tokens:
    ...         t = re.sub(r"(...)our$", r"\1or", t)
    ...         t = re.sub(r"([bt])re$", r"\1er", t)
    ...         t = re.sub(r"([iy])s(e$|ing|ation)", r"\1z\2", t)
    ...         t = re.sub(r"ogue$", "og", t)
    ...         yield t
    ...
    >>> class CustomVectorizer(CountVectorizer):
    ...     def build_tokenizer(self):
    ...         tokenize = super().build_tokenizer()
    ...         return lambda doc: list(to_british(tokenize(doc)))
    ...
    >>> print(CustomVectorizer().build_analyzer()(u"color colour"))
    [...'color', ...'color']
    

    لأساليب أخرى للمعالجة المُسبقة؛ تتضمن الأمثلة التصريف، التصريف، أو تطبيع الرموز المميزة الرقمية، مع توضيح الأخير في:

يمكن أن يكون تخصيص أداة المتجه مفيدًا أيضًا عند التعامل مع اللغات الآسيوية التي لا تستخدم فاصل كلمات صريحًا مثل المسافات البيضاء.

6.2.4. استخراج ميزات الصور#

6.2.4.1. استخراج الرقع#

تستخرج دالة extract_patches_2d الرقع من صورة مخزنة كمصفوفة ثنائية الأبعاد، أو ثلاثية الأبعاد مع معلومات اللون على طول المحور الثالث. لإعادة بناء صورة من جميع رقعها، استخدم reconstruct_from_patches_2d. على سبيل المثال، دعنا نُولِّد صورة 4x4 بكسل مع 3 قنوات لونية (على سبيل المثال بتنسيق RGB):

>>> import numpy as np
>>> from sklearn.feature_extraction import image

>>> one_image = np.arange(4 * 4 * 3).reshape((4, 4, 3))
>>> one_image[:, :, 0]  # قناة R لصورة RGB وهمية
array([[ 0,  3,  6,  9],
       [12, 15, 18, 21],
       [24, 27, 30, 33],
       [36, 39, 42, 45]])

>>> patches = image.extract_patches_2d(one_image, (2, 2), max_patches=2,
...     random_state=0)
>>> patches.shape
(2, 2, 2, 3)
>>> patches[:, :, :, 0]
array([[[ 0,  3],
        [12, 15]],

       [[15, 18],
        [27, 30]]])
>>> patches = image.extract_patches_2d(one_image, (2, 2))
>>> patches.shape
(9, 2, 2, 3)
>>> patches[4, :, :, 0]
array([[15, 18],
       [27, 30]])

دعونا الآن نحاول إعادة بناء الصورة الأصلية من الرقع عن طريق حساب المتوسط على المناطق المتداخلة:

>>> reconstructed = image.reconstruct_from_patches_2d(patches, (4, 4, 3))
>>> np.testing.assert_array_equal(one_image, reconstructed)

تعمل فئة PatchExtractor بنفس طريقة extract_patches_2d، إلا أنها تدعم صورًا متعددة كمدخلات. يتم تطبيقها كمُحوِّل scikit-learn، بحيث يمكن استخدامها في خطوط الأنابيب. انظر:

>>> five_images = np.arange(5 * 4 * 4 * 3).reshape(5, 4, 4, 3)
>>> patches = image.PatchExtractor(patch_size=(2, 2)).transform(five_images)
>>> patches.shape
(45, 2, 2, 3)

6.2.4.2. رسم بياني اتصال لصورة#

يمكن للعديد من المُقدِّرات في scikit-learn استخدام معلومات الاتصال بين الميزات أو العينات. على سبيل المثال، يمكن لتجميع Ward (التجميع الهرمي) تجميع وحدات البكسل المجاورة فقط لصورة معًا، وبالتالي تشكيل رقع مُتجاورة:

../_images/sphx_glr_plot_coin_ward_segmentation_001.png

لهذا الغرض، تستخدم المُقدِّرات مصفوفة "اتصال"، تُعطي العينات المتصلة.

تُعيد الدالة img_to_graph مثل هذه المصفوفة من صورة ثنائية أو ثلاثية الأبعاد. وبالمثل، grid_to_graph يبني مصفوفة اتصال لـ الصور بالنظر إلى شكل هذه الصور.

يمكن استخدام هذه المصفوفات لفرض الاتصال في المُقدِّرات التي تستخدم معلومات الاتصال، مثل تجميع Ward (التجميع الهرمي)، ولكن أيضًا لبناء نوى مُسبقة الحساب، أو مصفوفات تشابه.

ملاحظة

أمثلة