classifier.py
Wrapper همگام روی Hugging Face pipeline. بدون وابستگی به psycopg2. مدل یکبار در سازنده لود میشود؛ هر batch همان pipeline را reuse میکند. متن خالی به مدل نمیرسد.
Job دستهای که متن نقدها را با مدل Cardiff XLM-R طبقهبندی میکند و ستون reviews.sentiment را مینویسد — بدون دست زدن Storage Consumer به این ستون در upsert.
این مستند معماری، انتخاب مدل، فلوی اجرا، قرارداد مالکیت داده با Storage Consumer، نحوهٔ اجرا در venv و Docker، و استراتژی تست زیرسیستم Sentiment را شرح میدهد. این زیرسیستم یک job دستهای / CLI است: هر بار اجرا میشود، تمام نقدهای دارای محتوا را (بر اساس id) پیمایش میکند، برچسب احساس را با مدل دوباره محاسبه و overwrite میکند، سپس خارج میشود. Daemon دائمی نیست و با docker compose up معمولی بالا نمیآید — پشت پروفایل Compose به نام tools است.
مسیر دادهٔ نقدها اینگونه است: Crawler نقد را از Play Store میگیرد و به Kafka میفرستد → Storage Consumer آن را در جدول reviews مینشاند (بدون نوشتن sentiment) → این Job بعداً ستون sentiment را پر یا بهروز میکند. بنابراین Sentiment یک لایهٔ تحلیل پس از ماندگارسازی است، نه بخشی از مسیر ingest زنده.
مدل واحد پروژه:
cardiffnlp/twitter-xlm-roberta-base-sentiment
هیچ routing زبانی (مدل جدا برای فارسی/انگلیسی) وجود ندارد؛ همهٔ متنها از همین pipeline عبور میکنند.
| موضوع | نتیجه / تصمیم |
|---|---|
| ارزیابی داخلی | روی حدود ۹۰۰ رکورد برچسبخورده، دقت حدود ۹۲٪ بهدست آمد. در مقایسه با اکثر مدلهای هموزن (همرده از نظر اندازه/هزینه)، این مدل قویتر ظاهر شد و برای حجم و زبانهای پروژه انتخاب شد. |
| چرا نه LLM؟ | استفاده از LLMهای API-محور بررسی شد، اما لیمیت نرخ (rate limit) سنگین داشتند و برای حجم نقدهای پروژه مناسب نبودند: Job باید بتواند هزاران/دهها هزار ردیف را بدون صفشدن پشت سهمیهٔ ابری و بدون هزینهٔ پیشبینیناپذیر per-token جلو ببرد. مدل محلی Cardiff این محدودیت را ندارد. |
| محدودیت شناختهشدهٔ فارسی | این مدل رسماً روی فارسی fine-tune نشده؛ دقت فارسی عمدتاً از انتقال بینزبانی پایهٔ XLM-RoBERTa میآید. آزمایش routing به مدل تخصصی فارسی بهبود معناداری نشان نداد؛ بنابراین معماری تکمدلی برای سادگی حفظ شد. |
| خروجی مجاز | فقط POSITIVE / NEUTRAL / NEGATIVE — همتراز با CHECK constraint ستون reviews.sentiment. |
مثل Crawler و Network Analyzer، این زیرسیستم از کلاسهای پایتون خالص با تزریق وابستگی ساخته شده است. main.py تنها Composition Root است: Settings را لود میکند، اشیاء واقعی را میسازد، حلقهٔ batch را میچرخاند و در finally اتصال DB را میبندد.
Wrapper همگام روی Hugging Face pipeline. بدون وابستگی به psycopg2. مدل یکبار در سازنده لود میشود؛ هر batch همان pipeline را reuse میکند. متن خالی به مدل نمیرسد.
الگوی مشابه Storage Consumer: یک اتصال lazy، تراکنش صریح، timeoutهای session. Repository فقط fetch keyset و UPDATE دستهای با execute_values.
یک چرخه: خواندن صفحه → classify → (مگر dry-run) نوشتن. cursor داخلی after_id صفحه را جلو میبرد تا Job تمام شود.
هر اجرا از id = 0 شروع میکند و تا وقتی run_batch_cycle() مقدار True برگرداند (یعنی صفحهای کار داشته) حلقه ادامه دارد. برگشت False یعنی دیگر ردیفی بعد از cursor نیست — موفقیت با «کاری نمانده»، نه شکست.
| جزئیات | رفتار |
|---|---|
| Pagination | Keyset روی id (id > after_id) — مستقل از مقدار فعلی sentiment. |
| Overwrite | هر اجرای Job برچسبهای قبلی را دوباره مینویسد؛ برای اعمال مدل جدید یا اصلاح اشتباه مفید است. |
| Batch پیشفرض | SENTIMENT_BATCH_SIZE=100 (قابل override با --batch-size). |
| Truncation مدل | pipeline با truncation=True و max_length=512 صدا زده میشود تا نقدهای بلند خطا ندهند. |
| Exit codes | 0 موفقیت؛ 1 خطای config/classification/عمومی؛ 2 خطای argparse. |
sentimentستون در migration مربوط به Storage Consumer ساخته شده (با CHECK)، اما مالک نوشتن فقط این زیرسیستم است:
sentiment را لمس نمیکند — تا re-sync کراولر نتیجهٔ تحلیل را پاک نکند.sentiment بعد از upsert زنده میماند.UPDATE reviews SET sentiment = … بزند.Classifier برای متن None / خالی / فقط فاصله، مدل را صدا نمیزند و None برمیگرداند. سرویس آن را به NEUTRAL نگاشت میکند و مینویسد.
id است، نه روی «sentiment هنوز NULL است». نوشتن NEUTRAL فقط پیشفرض پایدار است.content است؛ مقدار فعلی sentiment در انتخاب دخالت ندارد.Postgres باید بالا باشد و جدول reviews (از طریق Storage Consumer / migrations) وجود داشته باشد. معمولاً اول استک را با bring_up یا حداقل postgres + مسیر ingest نقدها بالا میآورید، بعد Job را میزنید.
حتماً از ریشهٔ مخزن (PlayPulse/) اجرا کنید — نه از داخل پوشهٔ sentiment/ — تا importهای from sentiment... resolve شوند.
| آرگومان | معنا |
|---|---|
run (subcommand الزامی) | شروع Job: پیمایش کل نقدهای واجد شرایط و overwrite برچسبها. |
--batch-size N | جایگزین موقت SENTIMENT_BATCH_SIZE فقط برای همین اجرا (N > 0). |
--dry-run | طبقهبندی و لاگ شمارش POSITIVE/NEUTRAL/NEGATIVE؛ هیچ UPDATEای نوشته نمیشود. Cursor همچنان جلو میرود تا Job تمام شود. |
-v / --verbose | لاگ سطح DEBUG. |
sentiment/.env)فایل sentiment/.env فقط برای اجرای host/venv است. در Docker، آدرسها از docker-compose.yml تزریق میشوند و load_dotenv متغیرهای از قبل setشده را override نمیکند.
| متغیر | الزامی؟ | نقش |
|---|---|---|
POSTGRES_DB / USER / PASSWORD / HOST | بله | اتصال به همان دیتابیسی که Storage Consumer مینویسد. |
POSTGRES_PORT | خیر (۵۴۳۲) | پورت Postgres. |
SENTIMENT_MODEL_NAME | خیر | پیشفرض Cardiff XLM-R بالا. |
SENTIMENT_BATCH_SIZE | خیر (۱۰۰) | اندازهٔ صفحهٔ fetch/classify. |
SENTIMENT_DB_*_TIMEOUT_* | خیر | timeout اتصال و statement / idle-in-transaction. |
سرویس Compose: sentiment، پروفایل tools، image tag مشترک playpulse-sentiment:latest. Dockerfile مدل را داخل image bake میکند تا اولین run منتظر دانلود چندصدمگابایتی نماند. Torch بهصورت CPU-only نصب میشود.
docker compose up معمولی sentiment را بالا نمیآورد (پروفایل tools). bring_up.sh هم image سنتیمنت را cold-build نمیکند؛ فقط اگر playpulse-sentiment:latest از قبل موجود باشد ممکن است کانتینر را با --no-build بسازد — در غیر این صورت skip میکند. برای Job واقعی معمولاً باید صریحاً با compose run (و پروفایل tools) صدا زده شود. ENTRYPOINT برابر python -m sentiment.main است؛ آرگومانهای بعد از نام سرویس مستقیماً به CLI میرسند.| فایل | چیزی که ثابت میکند |
|---|---|
tests/unit/test_classifier.py |
نگاشت برچسبهای Cardiff (lowercase / Title / LABEL_N) به POSITIVE/NEUTRAL/NEGATIVE؛ متن خالی/None بدون صدا زدن مدل → None؛ حفظ ترتیب در batch ترکیبی؛ خطای ClassificationError برای برچسب ناشناخته؛ لود یکباره از طریق pipeline_factory تزریقی (بدون دانلود وزن). |
tests/unit/test_review_repository.py |
شکل SQL: فیلتر محتوا، keyset id > %s، عدم فیلتر روی ستون sentiment؛ پاسدادن after_id؛ UPDATE با execute_values؛ skip وقتی لیست نتایج خالی است. |
tests/integration/test_sentiment_e2e.py (مارکر integration): مسیر واقعی DB را تست میکند — ثبت اپ تستی، درج نقدها (بعضی با برچسب اشتباه از قبل)، اجرای یک run_batch_cycle، و اثبات overwrite. Classifier یک Stub مبتنی بر keyword است تا وزن مدل دانلود نشود؛ تمرکز تست روی fetch → write → re-read است. اگر Postgres یا env نباشد، تمیز skip میشود.
test)دو سرویس جدا، همان image سنگین playpulse-sentiment:latest:
| سرویس Compose | فرمان داخل کانتینر | نیاز |
|---|---|---|
sentiment-tests |
pytest sentiment/tests/unit -v |
Postgres healthy (depends_on)؛ تستها از host بهصورت volume mount میشوند چون Dockerfile لایهٔ tests/ را حذف میکند. |
sentiment-tests-integration |
pytest sentiment/tests/integration -v -m integration |
همینطور؛ env داخل شبکه Compose با POSTGRES_HOST=postgres. |
run_tests_in_docker.shاسکریپت ./scripts/run_tests_in_docker.sh هر دو سرویس sentiment-tests و sentiment-tests-integration را در صف اجرا دارد، اما عمداً sentiment را cold-build نمیکند. اگر image playpulse-sentiment:latest روی ماشین نباشد، آن ردیفها SKIPPED میشوند (تا یک build چنددقیقهای کل suite را بلوکه نکند). پس برای سبز شدن آنها در این اسکریپت، یکبار از قبل:
برای اطمینان از اینکه وزن مدل روی ماشین لود میشود و خروجی برچسب معقول است — جدا از pytest:
مستند اختصاصی این اسکریپت: manual_sentiment_check.py.
| مسیر | ارتباط با Sentiment |
|---|---|
scripts/manual_sentiment_check.py |
Smoke مدل واقعی Cardiff روی نمونههای en/fa بدون دیتابیس. |
scripts/run_tests_in_docker.sh |
اجرای کانتینرهای تست sentiment در صف کلی؛ skip اگر image نباشد. |
scripts/bring_up.sh |
استک داده (Postgres و …) را بالا میآورد؛ خودِ Job sentiment را بهصورت سرویس دائمی استارت نمیکند — بعداً باید compose run sentiment بزنید. |
Compose sentiment / sentiment-tests* |
تعریف build، env شبکه، و entrypointهای اجرا/تست در docker-compose.yml. |
دقت ≈۹۲٪ روی ≈۹۰۰ نمونه و برتری نسبی نسبت به مدلهای هموزن، همراه با نبود rate limit ابری، برای حجم دادهٔ پروژه عملیتر از LLMهای محدود به سهمیه بود.
Routing به مدل فارسی تخصصی بهبود معنادار نداد؛ پیچیدگی عملیاتی دو مدل (دو cache، دو مسیر خطا) پذیرفته نشد.
هر اجرا کل ردیفهای واجد شرایط را دوباره برچسب میزند تا تعویض مدل یا اصلاح باگ بدون migration دستی ممکن باشد. Pagination روی id جلوی حلقهٔ بینهایت را میگیرد.
sentimentStorage Consumer عمداً این ستون را در upsert دست نمیزند تا مسیر crawl بتواند بدون پاک کردن تحلیل، نقدها را تازه کند.
tools + bake مدل در imageاستک روزمره با سرویس چندگیگابایتی torch شلوغ نمیشود؛ وقتی Job لازم است، image از قبل مدل را دارد و اولین run سریعتر است.
دانلود وزن نباید پیششرط هر pytest سریع باشد؛ e2e روی قرارداد DB تمرکز دارد؛ smoke دستی فقط وقتی کیفیت مدل مهم است.
.env هاست جدا از env داکرمثل بقیهٔ زیرسیستمها: روی host معمولاً 127.0.0.1؛ داخل Compose postgres — تا فایل توسعه شبکهٔ داخلی را خراب نکند.
| ابزار | نقش |
|---|---|
| transformers (Hugging Face) | Pipeline تحلیل احساس و لود مدل Cardiff XLM-R |
| torch (CPU wheel) | موتور اجرای مدل؛ نصب جداگانه برای جلوگیری از wheel غولپیکر CUDA |
| sentencepiece | وابستگی tokenizer مدلهای XLM-R |
| psycopg2 | اتصال و تراکنش PostgreSQL؛ execute_values برای UPDATE دستهای |
| python-dotenv | بارگذاری sentiment/.env در اجرای host |
| pytest | Unit و Integration با مارکر integration |
Docker Compose profiles tools / test | اجرای Job و کانتینرهای تست بدون قاطی شدن با استک پیشفرض |