التحدي
في هذه المقالة ، نريد أن نتحدث عن كيفية إنشاء حل لتصنيف أسماء المنتجات من الإيصالات في التطبيق لتسجيل نفقات الشيكات ومساعد التسوق. أردنا منح المستخدمين الفرصة لعرض إحصاءات عن عمليات الشراء ، يتم جمعها تلقائيًا على أساس الإيصالات الممسوحة ضوئيًا ، أي توزيع جميع السلع التي اشتراها المستخدم حسب الفئة. لأن إجبار المستخدم على تجميع المنتجات بشكل مستقل هو بالفعل القرن الماضي. هناك العديد من الطرق لحل هذه المشكلة: يمكنك محاولة تطبيق خوارزميات التجميع بطرق مختلفة لتمثيل المتجهات للكلمات أو خوارزميات التصنيف الكلاسيكية. لم نبتكر أي شيء جديد ، وفي هذه المقالة نريد فقط مشاركة دليل صغير حول حل محتمل للمشكلة ، وأمثلة على كيفية عدم القيام بذلك ، وتحليل لماذا لم تنجح الطرق الأخرى والمشكلات التي قد تواجهها في العملية.
التكتل
كانت إحدى المشاكل هي أن أسماء البضائع التي نحصل عليها من الشيكات ليس من السهل دائمًا فكها ، حتى بالنسبة لشخص. من غير المحتمل أن تعرف نوع المنتج الذي يحمل اسم
"UTRUSTA krnsht" الذي تم شراؤه في أحد المتاجر الروسية؟ بالتأكيد سوف يجيبنا خبراء حقيقيون في التصميم السويدي على الفور: قوس لفرن Utrust ، ولكن الاحتفاظ بمثل هؤلاء المتخصصين في المقر مكلف للغاية. بالإضافة إلى ذلك ، لم يكن لدينا عينة جاهزة وملصقة مناسبة لبياناتنا ، والتي يمكننا من خلالها تدريب النموذج. لذلك ، سنتحدث أولاً عن كيف ، في حالة عدم وجود بيانات للتدريب ، قمنا بتطبيق خوارزميات التجميع ولماذا لم نحبها.
تعتمد هذه الخوارزميات على قياس المسافات بين الأشياء ، الأمر الذي يتطلب تمثيلها المتجه أو استخدام مقياس لقياس تشابه الكلمات (على سبيل المثال ، مسافة Levenshtein). في هذه الخطوة ، تكمن الصعوبة في التمثيل المتجه الهادف للأسماء. من الصعب استخراج الخصائص من الأسماء التي تصف المنتج بشكل كامل وشامل وعلاقته بالمنتجات الأخرى.
الخيار الأسهل هو استخدام Tf-Idf ، ولكن في هذه الحالة يكون حجم مساحة المتجه كبيرًا جدًا ، والمساحة نفسها قليلة. بالإضافة إلى ذلك ، هذا الأسلوب لا يستخرج أي معلومات إضافية من الأسماء. وبالتالي ، في مجموعة واحدة ، يمكن أن يكون هناك العديد من المنتجات من فئات مختلفة ، توحدها كلمة شائعة ، مثل ، على سبيل المثال ، "البطاطا" أو "السلطة":
لا يمكننا أيضًا التحكم في المجموعات التي سيتم تجميعها. الشيء الوحيد الذي يمكن الإشارة إليه هو عدد المجموعات (إذا تم استخدام خوارزميات تستند إلى قمم غير كثيفة في الفضاء). ولكن إذا حددت كمية صغيرة جدًا ، فسيتم تكوين مجموعة ضخمة واحدة ، والتي ستحتوي على جميع الأسماء التي لا يمكن احتواؤها في مجموعات أخرى. إذا حددت مجموعة كبيرة بما فيه الكفاية ، فعندئذٍ بعد عمل الخوارزمية ، سيتعين علينا النظر في مئات المجموعات ودمجها في فئات دلالية يدويًا.
توفر الجداول أدناه معلومات حول المجموعات التي تستخدم خوارزميات KMeans و Tf-Idf لتمثيل المتجهات. من هذه الجداول نرى أن المسافات بين مراكز العناقيد أقل من متوسط المسافة بين الأشياء ومراكز العناقيد التي تنتمي إليها. يمكن تفسير هذه البيانات بحقيقة أنه في فضاءات المتجهات لا توجد قمم كثافة واضحة وأن مراكز التجمعات تقع حول الدائرة ، حيث توجد معظم الكائنات خارج هذه الدائرة. بالإضافة إلى ذلك ، يتم تكوين مجموعة واحدة تحتوي على معظم المتجهات. على الأرجح في هذه المجموعة سوف تذهب إلى أسماء تحتوي على كلمات يتم العثور عليها في كثير من الأحيان من بين جميع المنتجات من فئات مختلفة.
الجدول 1. المسافات بين العناقيد.الكتلة | ج 1 | ج 2 | ج 3 | ج 4 | ج 5 | ج 6 | ج 7 | ج 8 | ج 9 |
---|
ج 1 | 0.0 | 0.502 | 0.354 | 0.475 | 0.481 | 0.527 | 0.498 | 0.501 | 0.524 |
---|
ج 2 | 0.502 | 0.0 | 0.614 | 0.685 | 0.696 | 0.728 | 0.706 | 0.709 | 0.725 |
---|
ج 3 | 0.354 | 0.614 | 0.0 | 0.590 | 0.597 | 0.635 | 0.610 | 0.613 | 0.632 |
---|
ج 4 | 0.475 | 0.685 | 0.590 | 0.0 | 0.673 | 0.709 | 0.683 | 0.687 | 0.699 |
---|
ج 5 | 0.481 | 0.696 | 0.597 | 0.673 | 0.0 | 0.715 | 0.692 | 0.694 | 0.711 |
---|
ج 6 | 0.527 | 0.727 | 0.635 | 0.709 | 0.715 | 0.0 | 0.726 | 0.728 | 0.741 |
---|
ج 7 | 0.498 | 0.706 | 0.610 | 0.683 | 0.692 | 0.725 | 0.0 | 0.707 | 0.714 |
---|
ج 8 | 0.501 | 0.709 | 0.612 | 0.687 | 0.694 | 0.728 | 0.707 | 0.0 | 0.725 |
---|
ج 9 | 0.524 | 0.725 | 0.632 | 0.699 | 0.711 | 0.741 | 0.714 | 0.725 | 0.0 |
---|
الجدول 2. معلومات موجزة عن العناقيدالكتلة | عدد الأشياء | متوسط المسافة | المسافة الدنيا | المسافة القصوى |
---|
ج 1 | 62530 | 0.999 | 0.041 | 1.001 |
---|
ج 2 | 2159 | 0.864 | 0.527 | 0.964 |
---|
ج 3 | 1099 | 0.934 | 0.756 | 0.993 |
---|
ج 4 | 1292 | 0.879 | 0.733 | 0.980 |
---|
ج 5 | 746 | 0.875 | 0.731 | 0.965 |
---|
ج 6 | 2451 | 0.847 | 0.719 | 0.994 |
---|
ج 7 | 1133 | 0.866 | 0.724 | 0.986 |
---|
ج 8 | 876 | 0.863 | 0.704 | 0.999 |
---|
ج 9 | 1879 | 0.849 | 0.526 | 0.981 |
---|
ولكن في بعض الأماكن ، تتضح أن العناقيد مناسبة تمامًا ، على سبيل المثال ، في الصورة أدناه - هناك ، جميع المنتجات تقريبًا من طعام القطط.
Doc2Vec هو آخر من الخوارزميات التي تسمح لك بتمثيل النصوص في شكل متجه. باستخدام هذا الأسلوب ، سيتم وصف كل اسم بمتجه ذي أبعاد أصغر من استخدام Tf-Idf. في الفضاء المتجه الناتج ، ستكون النصوص المتشابهة قريبة من بعضها البعض ، ونصوص مختلفة بعيدة.
يمكن أن يحل هذا النهج مشكلة البعد الكبير والمساحة المفرغة التي تم الحصول عليها بواسطة طريقة Tf-Idf. بالنسبة لهذه الخوارزمية ، استخدمنا أبسط خيار للترميز: قمنا بتقسيم الاسم إلى كلمات منفصلة وأخذنا أشكالها الأولية. تم تدريبه على البيانات بهذه الطريقة:
max_epochs = 100 vec_size = 20 alpha = 0.025 model = doc2vec.Doc2Vec(vector_size=vec_size, alpha=alpha, min_alpha=0.00025, min_count=1, dm =1) model.build_vocab(train_corpus) for epoch in range(max_epochs): print('iteration {0}'.format(epoch)) model.train(train_corpus, total_examples=model.corpus_count, epochs=model.iter)
ولكن مع هذا النهج ، حصلنا على ناقلات لا تحمل معلومات حول الاسم - بنفس النجاح يمكنك استخدام قيم عشوائية. فيما يلي مثال على تشغيل الخوارزمية: تعرض الصورة منتجات مماثلة في رأي الخوارزمية إلى "خبز Borodino من النموذج n pn 0.45k".
ولعل المشكلة تكمن في طول الأسماء وسياقها: يمكن أن يكون التمرير باسم "__ club. Banana 200ml" إما زبادي أو عصير أو علبة كبيرة من الكريمة. يمكنك تحقيق نتيجة أفضل باستخدام نهج مختلف لتسمية الاسم. لم تكن لدينا خبرة في استخدام هذه الطريقة ، وبحلول الوقت الذي فشلت فيه المحاولات الأولى ، وجدنا بالفعل مجموعتين ملحوظتين بأسماء منتجات ، لذلك قررنا ترك هذه الطريقة مؤقتًا والتبديل إلى خوارزميات التصنيف.
التصنيف
معالجة البيانات
إن أسماء السلع من الشيكات تأتي إلينا بطريقة غير واضحة دائمًا: اللاتينية والسيريلية مختلطة في الكلمات. على سبيل المثال ، يمكن استبدال الحرف "a" بالحرف "a" اللاتيني ، وهذا يزيد من عدد الأسماء الفريدة - على سبيل المثال ، سيتم اعتبار كلمتي "لبن" و "حليب" مختلفتين. تحتوي الأسماء أيضًا على العديد من الأخطاء الإملائية والاختصارات الأخرى.
لقد فحصنا قاعدة بياناتنا ووجدنا أخطاء نموذجية في الأسماء. في هذه المرحلة ، تم الاستغناء عن التعبيرات المنتظمة ، والتي قمنا من خلالها بتنظيف الأسماء وإحضارها إلى وجهة نظر عامة معينة. باستخدام هذا النهج ، يتم زيادة النتيجة بنسبة 7٪ تقريبًا. إلى جانب خيار مصنف SGD بسيط يعتمد على وظيفة فقدان Huber مع المعلمات الملتوية ، حصلنا على دقة 81٪ لـ F1 (متوسط الدقة لجميع فئات المنتجات).
sgd_model = SGDClassifier() parameters_sgd = { 'max_iter':[100], 'loss':['modified_huber'], 'class_weight':['balanced'], 'penalty':['l2'], 'alpha':[0.0001] } sgd_cv = GridSearchCV(sgd_model, parameters_sgd,n_jobs=-1) sgd_cv.fit(tf_idf_data, prod_cat) sgd_cv.best_score_, sgd_cv.best_params_
أيضًا ، لا تنس أن بعض فئات الأشخاص تشتري أكثر من غيرها: على سبيل المثال ، "الشاي والحلويات" و "الخضار والفواكه" أكثر شيوعًا من "الخدمات" و "مستحضرات التجميل". مع هذا التوزيع للبيانات ، من الأفضل استخدام الخوارزميات التي تسمح لك بتعيين الأوزان (درجة الأهمية) لكل فئة. يمكن تحديد وزن الفئة بشكل عكسي بالقيمة التي تساوي نسبة عدد المنتجات في الفئة إلى إجمالي عدد المنتجات. ولكن لا داعي للتفكير في الأمر ، لأنه عند تنفيذ هذه الخوارزميات ، من الممكن تحديد وزن الفئات تلقائيًا.
الحصول على بيانات جديدة للتدريب
تطلب تطبيقنا فئات مختلفة قليلاً عن تلك التي تم استخدامها في المسابقة ، وكانت أسماء المنتجات من قاعدة بياناتنا مختلفة بشكل كبير عن تلك المعروضة في المسابقة. لذلك ، كنا بحاجة إلى وضع علامة على البضائع من إيصالاتنا. حاولنا القيام بذلك بمفردنا ، لكننا أدركنا أنه حتى لو قمنا بتوصيل فريقنا بأكمله ، فسيستغرق الأمر وقتًا طويلاً جدًا. لذلك ، قررنا استخدام
"Toloka" في Yandex.
استخدمنا هذا الشكل من المهمة:
- في كل خلية قدمنا منتجًا ، يجب تحديد فئته
- فئتها الافتراضية المحددة بواسطة أحد النماذج السابقة لدينا
- مجال الاستجابة (إذا كانت الفئة المقترحة غير صحيحة)
لقد أنشأنا تعليمات تفصيلية مع أمثلة توضح ميزات كل فئة ، واستخدمنا أيضًا طرق مراقبة الجودة: مجموعة من الإجابات القياسية التي تم عرضها جنبًا إلى جنب مع المهام المعتادة (قمنا بتنفيذ الإجابات القياسية بأنفسنا ، مع ترميز عدة مئات من المنتجات). وفقًا لنتائج الإجابات على هذه المهام ، تم حجب المستخدمين الذين قاموا بترميز البيانات بشكل غير صحيح. ومع ذلك ، بالنسبة للمشروع بأكمله ، قمنا بحظر ثلاثة فقط من أكثر من 600 مستخدم.
مع البيانات الجديدة ، حصلنا على نموذج يناسب بياناتنا بشكل أفضل ، وزادت الدقة أكثر قليلاً (بنسبة 11٪ تقريبًا) وحصلت بالفعل على 92٪.
النموذج النهائي
بدأنا عملية التصنيف بمزيج من البيانات من عدة مجموعات بيانات مع Kaggle - 74٪ ، وبعد ذلك قمنا بتحسين المعالجة المسبقة - 81٪ ، وجمعنا مجموعة بيانات جديدة - 92٪ وأخيرًا قمنا بتحسين عملية التصنيف: في البداية ، باستخدام الانحدار اللوجستي نحصل على احتمالات أولية تنتمي السلع أعطت SGD دقة أكبر للفئات بناءً على أسماء المنتجات ، ولكن لا تزال لديها قيم كبيرة على وظائف الخسارة ، مما أثر على نتائج المصنف النهائي بشكل سيئ. علاوة على ذلك ، نقوم بدمج البيانات التي تم الحصول عليها مع البيانات الأخرى حول المنتج (سعر المنتج ، المتجر الذي تم شراؤه فيه ، إحصائيات عن المتجر ، الشيك والمعلومات التعريفية الأخرى) ، ويتم تدريب XGBoost على كل حجم البيانات هذا ، والذي أعطى دقة بنسبة 98٪ (زيادة 6٪ أخرى). كما اتضح ، فإن أعظم مساهمة تمت من جودة عينة التدريب.
يعمل على الخادم
لتسريع عملية النشر ، قمنا برفع خادم بسيط على Flask إلى Docker. كانت هناك طريقة واحدة لاستلام البضائع من الخادم والتي يجب تصنيفها وإرجاع البضائع مع الفئات بالفعل. وبالتالي ، تم دمجنا بسهولة في النظام الحالي ، الذي كان مركزه هو Tomcat ، ولم يكن علينا إجراء تغييرات على الهندسة المعمارية - لقد أضفنا للتو كتلة واحدة أخرى إليه.
تاريخ الإصدار
نشرنا منذ بضعة أسابيع إصدار تصنيف على Google Play (سيظهر في App Store بعد فترة). اتضح مثل هذا:
في الإصدارات المستقبلية ، نخطط لإضافة القدرة على تصحيح الفئات ، مما سيسمح لنا بجمع أخطاء التصنيف بسرعة وإعادة تدريب نموذج التصنيف (بينما نقوم بذلك بأنفسنا).
المسابقات المذكورة في Kaggle:
www.kaggle.com/c/receipt-categorisationwww.kaggle.com/c/market-basket-analysiswww.kaggle.com/c/prod-price-prediction