عنوان:

‫کالبدشکافی معماری الگوریتم توصیه‌گر X: از بازیابی تا رتبه‌بندی و پیاده‌سازی در دات‌نت


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۷ ۰۸:۵۰
آدرس: www.dntips.ir
چکیده: با متن‌باز شدن بخش‌های کلیدی مخزن الگوریتم پلتفرم X (توییتر سابق)، امکان تحلیل دقیق زیرساخت پیشنهاد محتوا (Recommendation System) در مقیاس بسیار بزرگ فراهم شده است. این سیستم بر خلاف مدل‌های سنتی که بر محبوبیت سراسری متمرکز بودند، بر شخصی‌سازی بلادرنگ (Real-time Personalization) و پیش‌بینی چندهدفه رفتار کاربر (Multi-Task Learning / User Action Prediction) تکیه دارد. در این مقاله، معماری چندمرحله‌ای خط لوله توزیع محتوا (Candidate Generation، Scoring & Ranking، و Visibility Filtering) بررسی می‌شود. همچنین فناوری‌ها و مفاهیمی چون Two-Tower Neural Networks، SimClusters، گراف اعتبار user-cred-v2 و اثر کاهشی جریمه‌های منفی به همراه تنوع‌بخشی به فید بازخوانی خواهند شد. در پایان، یک پیاده‌سازی ماژولار، استاندارد و شیءگرا با زبان #C و فریم‌ورک دات‌نت برای شبیه‌سازی این خط لوله ارائه می‌شود.

۱. مقدمه
در سیستم‌های توزیع محتوای کلان‌داده، پردازش صدها میلیون پست و میلیاردها کاربر به‌صورت بلادرنگ چالش بزرگی در حوزه تأخیر (Latency) و توان عملیاتی (Throughput) محسوب می‌شود. الگوریتم فید For You در X با تغییر پارادایم از «رتبه‌بندی بر مبنای شمارش ایستا» به سمت «پیش‌بینی بردار رفتار احتمالی کاربر»، سعی در بیشینه‌سازی ارزش ویژه برای هر کاربر در هر لحظه دارد:
+-------------------------------------------------------------------------+
|                              Candidate Generation                       |
|   In-Network (Thunder)  +  Out-of-Network / OON (Phoenix Two-Tower,     |
|                            SimClusters)                                 |
+-------------------------------------------------------------------------+
                                    |
                                    v
+-------------------------------------------------------------------------+
|                              Pre-Filtering                              |
|   AgeFilter (e.g. <= 48h), Blocklist, Hard Content Filters              |
+-------------------------------------------------------------------------+
                                    |
                                    v
+-------------------------------------------------------------------------+
|                         Scoring & Heavy Ranking                         |
|   Probability Prediction: P(Action_i) * Weight_i                        |
|   User Credential Graph (user-cred-v2 PageRank)                         |
+-------------------------------------------------------------------------+
                                    |
                                    v
+-------------------------------------------------------------------------+
|                  Diversity & Visibility Rules (Hydration)               |
|   Author Diversity Decay, Safety Labels (ALLOW / INTERSTITIAL / DROP)   |
+-------------------------------------------------------------------------+
                                    |
                                    v
                             Rendered For You


۲. بررسی ساختار فنی و اجزای خط لوله (Pipeline)
۲.۱. مرحله بازیابی کاندیداها (Candidate Generation)
در مرحله اول، سیستم باید از میان صدها میلیون توییت، چند هزار کاندیدای برتر را استخراج کند. این کاندیداها از دو منبع اصلی تامین می‌شوند:
- محتوای داخل شبکه (In-Network via Thunder): پایش گراف ارتباطی و استخراج پست‌های کاربرانی که فرد صراحتاً دنبال (Follow) کرده است.
- محتوای خارج از شبکه (Out-of-Network / OON via Phoenix & SimClusters):
  • معماری Two-Tower در Phoenix: شبکه عصبی که بردار ویژگی کاربر (Embedding_User) و بردار محتوا (Embedding_Post) را در فضایی چندبعدی می‌نشاند. شباهت این دو بردار (مثلاً از طریق کسینوس زاویه یا Dot Product) تخمین زده می‌شود.
  • خوشه‌بندی SimClusters: گروه‌بندی کاربران بر اساس گراف علاقه‌مندی‌ها و تعاملات مشترک. در صورتی که کاربری به خوشه‌ای از مباحث (مانند .NET، DevOps و Cloud) علاقه نشان دهد، محتوای ترند آن خوشه فارغ از رابطه Follow به او پیشنهاد می‌شود.

۲.۲. فیلترهای اولیه (Pre-Filtering & AgeFilter)
پیش از ارسال کاندیداها به مدل‌های سنگین یادگیری ماشین، فیلترهای سبک اعمال می‌شوند. به عنوان مثال، AgeFilter معمولاً پست‌های قدیمی‌تر از یک بازه زمانی معین (مانند ۴۸ ساعت در مسیرهای پرسرعت) را از چرخه کاندیداتوری داغ خارج می‌کند تا فید تازگی زمانی (Freshness) خود را حفظ کند.

۲.۳. رتبه‌بندی چندهدفه (Multi-Task Heavy Ranking)
هسته اصلی رتبه‌بندی، پیش‌بینی احتمال وقوع مجموعه‌ای از رخدادها (P(E_i)) است. امتیاز نهایی (Score) از حاصل‌ضرب احتمال در وزن تعیین‌شده محاسبه می‌گردد:

  • سیگنال‌های با هزینه شناختی بالا (High-Intent Engagement): اکشن‌هایی نظیر Copy Link، DM Share، Reply و Quote وزن بسیار بالاتری نسبت به یک Like ساده دارند، زیرا نشان‌دهنده ارزش واقعی محتوا برای نگهداری یا ارسال به دیگران هستند.
  • فاکتور ماندگاری (Dwell Time): توقف کاربر روی پست (مثلاً خواندن بیش از چند ثانیه بدون اسکرول سریع) سیگنالی مثبت برای مدل است.
  • قدرت تخریبی سیگنال‌های منفی: وزن اکشن‌های منفی مانند Report، Mute و Not Interested بسیار سنگین است؛ بنابراین محتواهایی با استراتژی خشم‌آفرینی (Rage Bait) در صورت دریافت گزارش یا میوت بالا، بلافاصله افت رتبه شدیدی پیدا می‌کنند.

۲.۴. گراف اعتبار کاربر (user-cred-v2)
مشابه الگوریتم PageRank در وب، در این پلتفرم نیز وزن گراف تعاملی کاربر محاسبه می‌شود. اعتبار پستی که از سوی یک اکانت با رتبه اعتباری بالا Retweet یا Reply شود، به مراتب بیشتر از تعامل حساب‌های اسپم یا نامعتبر است.

۲.۵. فیلترهای تنوع و دیده‌شدن (Visibility & Diversity)
- Author Diversity Decay: اعمال ضریب کاهش نمایی یا تدریجی بر امتیاز پست‌های متوالی یک نویسنده خاص، تا فید دچار انحصار یک تولیدکننده محتوا نشود.
- Visibility Filtering: جداسازی لایه رتبه‌بندی از لایه ایمنی. حتی در صورت کسب بالاترین امتیاز، قوانین پلتفرم وضعیت محتوا را مشخص می‌کنند:
  • ALLOW: نمایش عادی.
  • INTERSTITIAL: نمایش محتوا همراه با لایه هشدار (مانند محتوای حساس).
  • DROP: حذف کامل از فید توصیه‌ای.

۳. مدلسازی و پیاده‌سازی شیءگرا با #C در دات‌نت
برای درک عمیق‌تر این فرآیند، در ادامه یک پیاده‌سازی خوانا، تمیز و ساختاریافته از خط لوله رتبه‌بندی ارائه شده است.
using System;
using System.Collections.Generic;
using System.Linq;

namespace RecommendationEngine.Core
{
    public sealed record PostCandidate(
        string PostId,
        string AuthorId,
        DateTime CreatedAtUtc,
        bool IsInNetwork,
        double SimClusterAffinity
    );

    public sealed record ActionProbabilities(
        double PLike,
        double PReply,
        double PShareOrDm,
        double PCopyLink,
        double PFollowAuthor,
        double PNegativeSignal // تجمیع احتمال Report، Block یا Mute
    );

    public enum VisibilityStatus
    {
        Allow,
        Interstitial,
        Drop
    }

    public sealed record ScoredPost(
        PostCandidate Candidate,
        double RawScore,
        double FinalScore,
        VisibilityStatus Visibility
    );

    public interface ICandidateRanker
    {
        IEnumerable<ScoredPost> RankAndFilter(
            string userId,
            IEnumerable<PostCandidate> candidates,
            Func<PostCandidate, ActionProbabilities> inferenceModel,
            Func<PostCandidate, VisibilityStatus> safetyCheck,
            Func<string, double> getAuthorReputationScore
        );
    }

    public class ForYouFeedRanker : ICandidateRanker
    {
        // ضرایب تجربی متناسب با معماری سیستم
        private const double WeightLike = 0.5;
        private const double WeightReply = 5.0;
        private const double WeightShareOrDm = 5.0;
        private const double WeightCopyLink = 20.0;
        private const double WeightFollow = 4.0;
        private const double WeightNegative = 200.0;
        private const double AuthorDiversityDecay = 0.65;

        private readonly TimeSpan _maxAge = TimeSpan.FromHours(48);

        public IEnumerable<ScoredPost> RankAndFilter(
            string userId,
            IEnumerable<PostCandidate> candidates,
            Func<PostCandidate, ActionProbabilities> inferenceModel,
            Func<PostCandidate, VisibilityStatus> safetyCheck,
            Func<string, double> getAuthorReputationScore)
        {
            var now = DateTime.UtcNow;
            var scoredList = new List<ScoredPost>();

            foreach (var post in candidates)
            {
                // ۱. فیلتر سن پست (Age Filter)
                if (now - post.CreatedAtUtc > _maxAge)
                    continue;

                // ۲. ارزیابی اولیه ایمنی و دسترسی (Visibility Check)
                var visibility = safetyCheck(post);
                if (visibility == VisibilityStatus.Drop)
                    continue;

                // ۳. استنتاج احتمالات رفتار کاربر (Multi-Action Scoring)
                var probs = inferenceModel(post);

                double rawScore =
                    (probs.PLike * WeightLike) +
                    (probs.PReply * WeightReply) +
                    (probs.PShareOrDm * WeightShareOrDm) +
                    (probs.PCopyLink * WeightCopyLink) +
                    (probs.PFollowAuthor * WeightFollow) -
                    (probs.PNegativeSignal * WeightNegative);

                // ۴. ترکیب با وزن اعتبار کاربر و همبستگی خوشه‌ای
                double authorReputation = getAuthorReputationScore(post.AuthorId);
                double clusterMultiplier = post.IsInNetwork ? 1.0 : (1.0 + post.SimClusterAffinity);
                
                double calculatedScore = rawScore * authorReputation * clusterMultiplier;

                scoredList.Add(new ScoredPost(post, rawScore, calculatedScore, visibility));
            }

            // ۵. اعمال تنوع نویسندگان (Author Diversity Decay) و مرتب‌سازی نهایی
            return ApplyDiversityAndSort(scoredList);
        }

        private static IEnumerable<ScoredPost> ApplyDiversityAndSort(List<ScoredPost> posts)
        {
            var authorSeenCount = new Dictionary<string, int>();
            var finalList = new List<ScoredPost>(posts.Count);

            foreach (var item in posts.OrderByDescending(p => p.FinalScore))
            {
                var authorId = item.Candidate.AuthorId;
                authorSeenCount.TryGetValue(authorId, out int seenCount);

                // کاهش هندسی امتیاز برای پست‌های مکرر از یک نویسنده
                double decayFactor = Math.Pow(AuthorDiversityDecay, seenCount);
                double adjustedScore = item.FinalScore * decayFactor;

                authorSeenCount[authorId] = seenCount + 1;

                finalList.Add(item with { FinalScore = adjustedScore });
            }

            return finalList.OrderByDescending(p => p.FinalScore);
        }
    }

    public static class Program
    {
        public static void Main()
        {
            var ranker = new ForYouFeedRanker();

            var sampleCandidates = new List<PostCandidate>
            {
                new("P101", "DevMaster", DateTime.UtcNow.AddHours(-2), IsInNetwork: false, SimClusterAffinity: 0.8),
                new("P102", "DevMaster", DateTime.UtcNow.AddHours(-3), IsInNetwork: false, SimClusterAffinity: 0.8),
                new("P103", "ClickBaiter", DateTime.UtcNow.AddHours(-1), IsInNetwork: true, SimClusterAffinity: 0.2),
                new("P104", "SafeAuthor", DateTime.UtcNow.AddDays(-3), IsInNetwork: true, SimClusterAffinity: 0.5) // حذف بر اساس سن
            };

            var feed = ranker.RankAndFilter(
                userId: "User_99",
                candidates: sampleCandidates,
                inferenceModel: post => post.PostId switch
                {
                    "P101" => new ActionProbabilities(0.4, 0.2, 0.1, 0.08, 0.05, 0.001),
                    "P102" => new ActionProbabilities(0.3, 0.1, 0.05, 0.02, 0.01, 0.001),
                    "P103" => new ActionProbabilities(0.8, 0.5, 0.01, 0.00, 0.00, 0.04), // سیگنال منفی بالا
                    _ => new ActionProbabilities(0.1, 0.01, 0.0, 0.0, 0.0, 0.0)
                },
                safetyCheck: _ => VisibilityStatus.Allow,
                getAuthorReputationScore: author => author == "DevMaster" ? 1.4 : 1.0
            );

            Console.WriteLine($"{"PostId",-8} | {"Author",-12} | {"Raw Score",-10:F2} | {"Final Score",-12:F2} | Status");
            Console.WriteLine(new string('-', 55));

            foreach (var post in feed)
            {
                Console.WriteLine($"{post.Candidate.PostId,-8} | {post.Candidate.AuthorId,-12} | {post.RawScore,-10:F2} | {post.FinalScore,-12:F2} | {post.Visibility}");
            }
        }
    }
}

۴. جمع‌بندی و درس‌های کلیدی
تحلیل معماری متن‌باز پلتفرم X نشان می‌دهد که موتورهای پیشنهاد محتوای نوین، داده‌های تعاملی را به شکلی معنادار و نامتقارن ارزش‌گذاری می‌کنند:
  • انتقال از شمارش ایستا به استنتاج احتمالی: الگوریتم تعداد لایک‌ها را مقایسه نمی‌کند، بلکه احتمال رفتار کاربر نسبت به یک محتوای خاص را می‌سنجد.
  • ارزش تعاملات با تصمیم آگاهانه: رفتار‌هایی نظیر کپی کردن لینک، پیام خصوصی یا بازنشر ارزش به مراتب بالاتری نسبت به لایک‌های گذرا دارند.
  • شکست الگوهای خشم‌آفرینی (Rage Bait): به دلیل جریمه‌های سنگین سیگنال‌های منفی (Report و Mute)، رویکردهایی که منجر به نارضایتی بخشی از کاربران شوند، به سرعت از چرخه‌ی توزیع حذف می‌شوند.
  • توزیع‌پذیری حساب‌های کوچک: از طریق خوشه‌بندی برداری (SimClusters) و مدل‌های Two-Tower، یک محتوای باکیفیت حتی بدون داشتن دنبال‌کنندگان پرشمار، شانس دیده‌شدن گسترده در سطح شبکه را داراست.