当前位置: 首页 > news >正文

6. 从0到上线:.NET 8 + ML.NET LTR 智能类目匹配实战--渐进式学习闭环:从反馈到再训练

在这篇文章中,我们聚焦一个可落地的“渐进式学习”闭环,以 MongoDB 作为反馈中枢,把用户的“选择/纠正”即时沉淀为 LTR 可训练样本。通过置信度门控与四层规则回退,确保任意场景都能给出稳妥结果,并且以小批滑动窗口叠加历史全量的“伪增量”方式持续再训练模型。全部能力由统一的 Web API 控制器对外暴露预测、反馈、重训与统计,核心阈值、频率与批量大小等参数通过 appsettings 配置驱动,便于按环境与阶段自如调参。

闭环流程非常直观,当模型可用时优先走 LTR 排序,不可用或置信度偏低时回退到规则并要求确认。用户产生的选择或纠正会连同候选列表、用户、商户、时间与金额上下文一并入库,并被转换为分组一致、正负样本齐备的 LTR 训练对。系统按配置的频率或在纠正发生时立即触发再训练,利用最近窗口样本合并历史数据完成更新。训练完成后模型文件被保存,服务可热加载或在下一次启动时恢复。

一、预测分支:ML 与规则的协同

线上预测需要在“机器学习”与“规则回退”之间作出可解释、可控且稳健的决策,当存在可用模型且训练样本满足最低要求时,系统优先使用基于 LightGBM Ranking 的排序对一批候选统一打分(共享 GroupId),并用置信度阈值决定是否需要用户确认。当模型不可用、候选过少或评分低于阈值,则切换到四层规则回退(精确关键词 → 语义相似 → 通用扩展 → 智能默认),保证始终有结果且便于理解与干预。输入包括 Query 与候选类目,UserId/Merchant/AmountBucket/HourOfDay 作为上下文特征参与打分,从而在保持体验可用的同时最大化利用排序模型的泛化与个性化能力。

1.1 智能预测(SmartPredict)

SmartPredict 方法对一次请求的全部候选统一打分并择优返回,模型可用则走 LTR Top1,异常或低置信度时回退规则,并产出是否需要用户确认的标志。输入包括 Query、候选类目与 UserId/Merchant/AmountBucket/HourOfDay 上下文特征。核心逻辑SmartPredict的实现:

public SmartPredictionResult SmartPredict(string query, IEnumerable<UserCategory> categories,string userId = "",string merchant = "",float amountBucket = 0,float hourOfDay = 0)
{var categoriesList = categories.ToList();if (_matcher.TrainingDataCount == 0 || categoriesList.Count < 2){var fallbackResult = RuleBased_Fallback(query, categoriesList);return new SmartPredictionResult{PredictedCategory = fallbackResult,Confidence = 0.1f,Method = PredictionMethod.RuleBased,RequiresUserConfirmation = true};}try{var result = _matcher.PredictTop1(query, categoriesList, userId, merchant, amountBucket, hourOfDay);var requiresConfirmation = result.Score < _options.ConfidenceThreshold;return new SmartPredictionResult{PredictedCategory = result.Category,Confidence = result.Score,Method = PredictionMethod.MachineLearning,RequiresUserConfirmation = requiresConfirmation};}catch (Exception ex){Console.WriteLine($"ML prediction failed: {ex.Message}, falling back to rules");var fallbackResult = RuleBased_Fallback(query, categoriesList);return new SmartPredictionResult{PredictedCategory = fallbackResult,Confidence = 0.1f,Method = PredictionMethod.RuleBased,RequiresUserConfirmation = true};}
}

SmartPredict 方法首先将输入的类目枚举物化为列表,以避免多次枚举带来的副作用,并作为后续判断与调用的统一容器。接着进行两项快速短路判定,如果当前模型尚未训练(_matcher.TrainingDataCount == 0)或候选类目数量不足以形成有意义的排序(categoriesList.Count < 2),直接走规则回退。这里返回的 SmartPredictionResult 明确标注 Method = RuleBased 与较低的 Confidence = 0.1f,并将 RequiresUserConfirmation 设为 true,保证客户端引导用户确认,避免冷启动阶段的误判造成体验损伤。

当具备模型与充分候选时,进入 try 分支调用 _matcher.PredictTop1。该调用会在排序打分时同时引入上下文特征:userIdmerchantamountBuckethourOfDay,并对同一批候选使用一致的分组键,从而获得可比较的相对相关性分数。返回的 result 携带最佳类目与 Score,随后以 _options.ConfidenceThreshold 作为置信度门控,计算是否需要用户确认。最终将结果整合为 SmartPredictionResult 返回,其中的 Method = MachineLearningConfidence 即为排序得分,RequiresUserConfirmation 用于驱动前端的人机协同策略。

任何预测异常都会被捕获并记录到控制台日志中,同时回退到规则路径生成与冷启动一致的保守结果。这样设计的目的在于确保线上调用无论何时都能得到可用输出,并且在模型不可用或打分异常时具备一致的降级行为。阈值、最小训练数据量与回退策略并未硬编码在方法内部,而是通过 ProgressiveLearningOptions 与内部的规则函数进行解耦,这使得阈值调优与规则维护可以在不改动主流程的情况下独立演进。

1.2 四层规则回退

当模型不可用、候选不足或置信度低于阈值时,系统切换到规则路径,这个策略按从严到宽的四层递进:精确关键词匹配、语义相似匹配、通用关键词扩展与智能默认兜底,目标是在保证可解释与低风险的前提下,总能产出可用结果。实现流程先对 Query 归一化与字典查找,再做类名/同义词包含匹配,随后依据通用词推断领域,最后优先返回“其他/未分类”等安全类目。下面的代码是四层规则回退的核心代码:

private UserCategory RuleBased_Fallback(string query, List<UserCategory> categories)
{var queryLower = query.ToLowerInvariant();var rules = new Dictionary<string, string[]>{["餐饮"] = new[] { "餐", "吃", "喝", "茶", "咖啡", "外卖", "饭", "菜", "肯德基", "麦当劳", "星巴克" },["交通"] = new[] { "地铁", "公交", "出租", "滴滴", "车", "票", "油", "停车" },["数码"] = new[] { "手机", "电脑", "充电", "数码", "电子", "软件", "游戏" },["娱乐"] = new[] { "电影", "ktv", "游戏", "娱乐", "音乐", "视频", "直播" },["购物"] = new[] { "淘宝", "京东", "购物", "买", "商场", "超市" },["居家"] = new[] { "电费", "水费", "房租", "物业", "家具", "装修" },["医疗"] = new[] { "医院", "药", "体检", "看病", "牙科", "医疗" },["学习"] = new[] { "书", "教育", "培训", "学习", "课程", "学费" }};foreach (var category in categories){if (rules.TryGetValue(category.Name, out var keywords)){if (keywords.Any(k => queryLower.Contains(k))) return category;}}var semanticMatch = FindSemanticMatch(queryLower, categories);if (semanticMatch != null) return semanticMatch;var generalMatch = FindGeneralMatch(queryLower, categories);if (generalMatch != null) return generalMatch;var otherCategory = categories.FirstOrDefault(c => c.Name.Contains("其他") || c.Name.Contains("未分类") || c.Name.Contains("杂项"));return otherCategory ?? categories.FirstOrDefault() ?? new UserCategory("unknown", "未知");
}

RuleBased_Fallback 接受原始查询与用户的类目集合,并承诺永远返回一个非空的 UserCategory。首先将查询转换为小写的不可变文化形式(ToLowerInvariant),用来实现大小写无关的子串匹配,这对拉丁字符查询尤为重要。在中文场景下不会改变语义,但保证了英文品牌词与缩写的一致匹配行为。随后定义 rules 字典,将标准类目名映射到一组高频关键词,覆盖常见的品牌名与场景词。之所以选择字典而非串行 if-else,是为了 O(1) 命中类目规则并在每个类目内做 O(k) 的关键词扫描,整体复杂度约为 O(∑k)。

第一层“精确关键词匹配”通过遍历用户提供的类目列表,并使用 rules.TryGetValue(category.Name, out var keywords) 判断该类目是否有预定义关键词集,一旦命中,利用 keywords.Any(k => queryLower.Contains(k)) 做子串判断,命中即返回该类目。这一步优先级最高,能够在“星巴克”“麦当劳”这类强信号出现时直接给出确定答案,避免被更宽泛的规则抢占。Contains 的实现没有做分词或边界匹配,是基于记账领域高频短词的实用主义取舍。如果出现误触发,可将这一层替换为基于正则或词边界的判断以降低误报。

如果第一层未命中,就进入第二层“语义相似匹配”。FindSemanticMatch 内先做类目名与查询的双向包含匹配(类目包含查询或查询包含类目),兼容“用餐/餐饮”“地铁/交通”这类包含关系,再查询该类目的同义词集合(GetCategorySynonyms)并做同样的包含判断。这一层的作用是放宽精确关键词的限制,吸收常见的等价或近义表达,同时仍保持可解释性:命中原因可明确归结到“类目名/同义词命中”。

第三层“通用关键词扩展匹配”由 FindGeneralMatch 实现,聚焦跨领域的提示词,例如与金额相关的“钱/费/付/收/转账”,服务相关的“服务/维修/保养”,或礼品相关的“礼/送/买”。一旦命中这些通用提示词,函数会在用户类目中优先寻找与之语义相关的类目(如理财、服务、购物等),该层更强调“倾向性推断”,在缺少强语义证据时提供合理的猜测,仍然遵循“优先用户自有类目”的约束以避免越界分类。

如果前三层皆未覆盖时,进入第四层“智能默认兜底”。这里优先选择名称包含“其他/未分类/杂项”的安全类目,以降低误分类风险。如果用户并未提供此类兜底类目,则退而返回第一个类目。如果连候选都不存在,构造 new UserCategory("unknown", "未知"),确保函数的返回值非空,避免上层业务出现空引用异常。四层由严到宽的顺序设计,体现“能准则准、不能准则稳”的策略:先覆盖强信号,再扩展相似与通用,最后保守兜底,从而兼顾可解释性与线上稳定性。

在工程层面,这个回退函数是可演进的扩展点,rules 与同义词表可外置到配置或持久化存储中,以便 A/B 与灰度更新,Contains 可替换为正则或分词引擎以提升精度,不同语言/地域的差异可通过定制同义词与关键词列表来适配。由于所有路径都快速返回,且仅做常数级字符串操作,该实现对延迟极为友好,当需要处理超大类目集时,可以引入前缀树、倒排索引或缓存命中策略以进一步优化性能。

1.3 置信度阈值与确认机制

置信度阈值是线上人机协同的门闸。模型打分高于阈值可直接自动分类,低于阈值则以 RequiresUserConfirmation=true 交由客户端二次确认,既避免误判,又持续收集高价值纠正数据。阈值、最小训练数据量与重训频率统一由 ProgressiveLearningOptions 配置并可按环境差异化,阈值偏高更保守但交互更多,阈值偏低自动化更强但风险上升,建议结合低置信度占比与纠正率的线上分布定期校准。以下是阈值/频率等关键开关:

public sealed class ProgressiveLearningOptions
{public float ConfidenceThreshold { get; set; } = 0.35f;public int MinTrainingDataSize { get; set; } = 10;public int RetrainingFrequency { get; set; } = 5;public int IncrementalBatchSize { get; set; } = 10;
}

ConfidenceThreshold 用于把纯机器决策转化为人机协同的门闸,模型得分高于该阈值可以直接返回自动分类结果,低于阈值则在响应中标记 RequiresUserConfirmation=true,由前端引导用户确认或纠正。阈值越高,系统越保守,误判率下降但用户交互上升,反之阈值越低,自动化程度提高但需要用更强的回退与审核来承担风险。实践中应结合线上“低置信度占比”和“用户纠正率”的滑窗分布定期校准,并在活动促销等语义扰动明显的时段临时上调,以避免突发漂移带来的体验抖动。

MinTrainingDataSize 是再训练的闸门,防止在样本非常稀少时触发拟合不稳的模型更新。冷启动阶段数据增量往往呈现强偏置与重复,提前训练不仅收效甚微,还可能放大噪声。将门槛设为 10 的量级能够确保至少有若干组包含正负样本的 LTR 查询,便于 LightGBM 的分裂找到稳定的增益方向。当我们导入历史样本或批量标注后,可临时下调以加速首次上线,但应在达到稳定规模后恢复到更稳妥的阈值。

RetrainingFrequency 控制按反馈计数的周期触发策略,每累计 N 条反馈触发一次增量学习。频率越小,模型新鲜度越高,但 CPU/IO 成本与潜在的参数抖动也更大,频率越大则更新更稳但收敛更慢。结合你的流量与反馈密度,可以把该值与业务节奏对齐,例如工作日高峰期用更小的频率、夜间或周末上调,提高成本效率与稳定性。

IncrementalBatchSize 定义每次增量学习所采用的最近反馈窗口大小。由于此实现采取“伪增量”(用历史全量加上最近窗口重训),窗口大小直接影响对近期语义的适应速度与对历史记忆的保留比例。较小的窗口能更快贴近最新趋势,但对偶发噪声较敏感,较大的窗口能平滑噪声但响应变慢。建议将该值与 RetrainingFrequency 配合,在“每 M 次触发、用最近 K 条”这一组合上做小步试验,并用线上 NDCG、低置信度比例与纠正率的变化来判断是否达成更优的稳健-新鲜折中。

这些参数都来自 appsettings.json 并由依赖注入读入,天然支持多环境差异化配置与灰度调参。如果需要更高的运营弹性,可以把它们接到动态配置源或管理面板,通过热更新在不重启服务的前提下完成校准,并配合监控看板的分层指标做闭环验证。

1.4 实务建议(基于经验)

建议将置信度阈值初设在 0.35–0.5 的区间,阈值越高越保守、交互更多但整体质量更稳;结合线上低置信度占比与纠正率的变化逐步微调,以维持“稳健—效率”的平衡。

在小数据阶段,优先通过 TopK 在前端展示候选,并对低置信度结果启用强制确认,以加速积累纠正反馈、缩短冷启动时间;随着样本规模增长,再逐步放宽阈值或增强自动化比例。

二、反馈采集与数据治理(MongoDB)

2.1 反馈类型与数据模型(UserFeedback)

两类反馈覆盖用户“选择”与“纠正”,并完整保存候选列表、时间、商户、用户等上下文,为后续 LTR 样本化与治理提供足够信息。

public sealed class UserFeedback
{/// <summary>MongoDB文档ID,自动生成</summary>[BsonId][BsonRepresentation(BsonType.ObjectId)]public string? MongoId { get; set; }/// <summary>唯一标识符,用作训练数据的GroupId</summary>[BsonElement("id")]public string Id { get; set; } = string.Empty;/// <summary>用户查询文本(消费描述),主要的文本特征</summary>[BsonElement("query")]public string Query { get; set; } = string.Empty;/// <summary>用户选择的正确类目名称,训练时的正样本</summary>[BsonElement("selectedCategory")]public string SelectedCategory { get; set; } = string.Empty;/// <summary>系统错误预测的类目名称(仅纠正场景),训练时的负样本</summary>[BsonElement("wrongCategory")]public string? WrongCategory { get; set; }/// <summary>用户ID,用于个性化推荐特征</summary>[BsonElement("userId")]public string UserId { get; set; } = string.Empty;/// <summary>商户信息,用于上下文推断特征</summary>[BsonElement("merchant")]public string Merchant { get; set; } = string.Empty;/// <summary>金额桶值,用于金额相关的数值特征</summary>[BsonElement("amountBucket")]public float AmountBucket { get; set; }/// <summary>一天中的小时数(0-23),用于时间模式特征</summary>[BsonElement("hourOfDay")]public float HourOfDay { get; set; }/// <summary>当时可选的类目列表,用于生成完整的训练样本集</summary>[BsonElement("availableCategories")]public List<string> AvailableCategories { get; set; } = new();/// <summary>反馈记录的时间戳,用于排序和时间分析</summary>[BsonElement("timestamp")][BsonDateTimeOptions(Kind = DateTimeKind.Utc)]public DateTime Timestamp { get; set; }/// <summary>反馈类型,区分主动选择和错误纠正</summary>[BsonElement("feedbackType")][BsonRepresentation(BsonType.String)]public FeedbackType FeedbackType { get; set; }
}

这个数据模型本质上是一张高价值的行为事实表。FeedbackType 区分用户“选择”(UserChoice)与“纠正”(UserCorrection),后者通常更具学习价值并触发即时重训。Id 为业务侧唯一标识,采用 GUID 字符串并在训练阶段直接复用为 LTR 的 GroupId,保证同一次交互产生的候选样本天然分组,MongoId 则是数据库层的 ObjectId 主键,服务于存储与索引。

文本主特征由 Query 承载,SelectedCategory 表示用户最终认可的正确类目,在纠正场景下,WrongCategory 记录模型的错误预测,便于形成清晰的正负对照关系并支持离线误差分析。AvailableCategories 保存当时可供选择的候选集合,是生成完整 LTR 训练对的关键信息。如果该集合缺失或未包含正确类目,训练转换逻辑会自动补齐,以满足“每组至少一个正样本”的算法前提。

上下文特征用于提高排序模型在个性化与情境推断上的表现。UserId 反映个体偏好,Merchant 体现商户语义线索,AmountBucket 是离散化后的金额分箱,HourOfDay 则捕捉时间模式。保持这些字段在预测与训练路径的一致性至关重要,否则会造成特征漂移,影响线上稳定性与离线评估的可比性。

Timestamp 统一使用 UTC 存储,便于跨地域部署与稳定的时间窗口查询。围绕该字段建立的降序索引配合 GetRecentFeedbacks 能快速取回最近窗口,支撑“伪增量”再训练。对 UserIdFeedbackType 的索引则有助于用户级行为分析与类型分布监控。在容量增长后,可以按时间分段做冷热分层与归档,既保留训练价值,又控制主集合体量。

在数据质量与治理方面,应保证关键字段非空约束与基本清洗,例如去除异常空白、统一全角/半角与大小写、限制 AvailableCategories 的重复项。同时将潜在 PII 的访问最小化,必要时对 UserId 做哈希或映射脱敏,并用最小权限账户访问库。随着反馈规模提升,建议配套定期的数据质量报表与异常分布看板,为阈值调整与规则库迭代提供依据。

2.2 记录用户选择(RecordUserChoice)

该接口将用户对系统推荐或候选列表的正反馈落库,作为正样本的主要来源。它会连同当时的候选列表与 UserId/Merchant/AmountBucket/HourOfDay 等上下文一起保存,便于后续在 LTR 转换中生成完整的查询-候选对并标注正负样本;写入后按照配置的重训频率阈值决定是否触发增量学习,从而在新鲜度与稳定性之间取得平衡。

public void RecordUserChoice(string query, string selectedCategory, string userId = "", string merchant = "", float amountBucket = 0, float hourOfDay = 0, IEnumerable<string>? availableCategories = null)
{var feedback = new UserFeedback{Id = Guid.NewGuid().ToString("N"),Query = query,SelectedCategory = selectedCategory,UserId = userId,Merchant = merchant,AmountBucket = amountBucket,HourOfDay = hourOfDay,AvailableCategories = availableCategories?.ToList() ?? new List<string>(),Timestamp = DateTime.UtcNow,FeedbackType = FeedbackType.UserChoice};_feedbackCollection.InsertOne(feedback);
}

RecordUserChoice方法面向一次“正反馈”场景,将用户确认过的分类结果以完整上下文持久化。入参 query 是原始消费描述文本,selectedCategory 是最终被用户认可的正确类目。userIdmerchantamountBuckethourOfDay 提供个性化与情境信息,能够在后续 LTR 样本化与模型训练阶段作为类别与数值特征参与打分。availableCategories 捕捉当时对用户可见的候选集合,通过 availableCategories?.ToList() ?? new List<string>() 做了防空与物化,避免延迟枚举或空引用带来的不确定性,这里仅存储当时原始候选,稍后在样本转换时再确保“至少包含正确类目”的不变式。

方法体首先构造 UserFeedback 文档,Id = Guid.NewGuid().ToString("N") 生成一个去分隔符的 32 位 GUID 作为业务主键,这个值在训练阶段将被直接复用为 LTR 的 GroupId,从而保证同一次交互生成的查询-候选若干行天然属于同一组,有利于 LightGBM 排序学习正确建模相对相关性。QuerySelectedCategoryUserIdMerchantAmountBucketHourOfDayAvailableCategories 分别落入对应字段,Timestamp = DateTime.UtcNow 统一使用 UTC,方便跨时区部署与基于时间窗口的近端数据抽取,FeedbackType = FeedbackType.UserChoice 明确标注了这是用户选择而非纠正的正反馈,为离线对比分析与治理提供维度。

持久化通过 _feedbackCollection.InsertOne(feedback) 完成,MongoDB 驱动具备连接池与线程安全特性,适合在高并发 API 中直接调用。索引层面,结合预先创建的 Timestamp 降序索引,可以快速按时间获取最近 N 条反馈作为“伪增量”再训练的窗口。UserIdFeedbackType 的单键索引则支撑用户级分析与类型分布监控。这里没有使用 try/catch,是因为更上层的业务流程已统一处理异常并返回错误信息,如果面向更苛刻的生产环境,可以在此增加带退避的重试策略与幂等约束(例如以 Id 作为去重键)来抵御偶发网络抖动或重复提交。

Tip:RecordUserChoice 本身只负责原子化持久化,不直接触发样本转换或训练,在服务编排层(如 ProgressiveLearningManager.RecordUserChoice)会在写入后依据 RetrainingFrequency 判断是否触发增量学习。这样的分层让数据采集与训练调度解耦,便于独立扩展索引策略、数据清洗与合规治理,同时允许在不同运行环境下以配置化的方式调整重训节奏与窗口大小。

2.3 记录用户纠正(RecordUserCorrection)

记录用户纠正接口用于记录用户对系统错误预测的纠正,既保存模型给出的 wrongCategory,也保存用户确认的 correctCategory,并连同当时的候选集合与 UserId/Merchant/AmountBucket/HourOfDay 等上下文一并入库,便于在 LTR 转换时生成成组的正负样本并保持 GroupId 一致性。相比“选择”数据,纠正样本能够更直接地暴露模型的判错边界,学习价值更高,通常作为高权重的近期增量并默认触发即时重训,以尽快修复最近出现的错误模式。以下是核心代码:

public void RecordUserCorrection(string query, string wrongCategory, string correctCategory, string userId = "", string merchant = "", float amountBucket = 0, float hourOfDay = 0,IEnumerable<string>? availableCategories = null)
{var feedback = new UserFeedback{Id = Guid.NewGuid().ToString("N"),Query = query,SelectedCategory = correctCategory,WrongCategory = wrongCategory,UserId = userId,Merchant = merchant,AmountBucket = amountBucket,HourOfDay = hourOfDay,AvailableCategories = availableCategories?.ToList() ?? new List<string>(),Timestamp = DateTime.UtcNow,FeedbackType = FeedbackType.UserCorrection};_feedbackCollection.InsertOne(feedback);
}

RecordUserCorrection方法用于将一次“负转正”的高价值反馈完整落库。query 承载原始消费描述,wrongCategory 明确记录模型给出的错误预测,correctCategory 则是用户确认的正确答案。二者的并置能在后续样本化时直接生成一组对照鲜明的正负样本。userIdmerchantamountBuckethourOfDay 作为上下文特征与选择场景保持一致,保证训练与预测路径的特征同构,降低分布漂移。availableCategories 捕捉当时候选集合并在入库时做防空与物化处理,后续 LTR 转换阶段若缺失正确类目会自动补齐,以满足排序学习每组至少一个正样本的前提。

构造 UserFeedback 时,Id = Guid.NewGuid().ToString("N") 作为业务主键,并在训练阶段复用为 GroupId,从而确保同一次纠正交互的所有候选样本天然同组,LightGBM 能够在组内学习相对相关性。与“选择”不同,这里同时写入 SelectedCategory = correctCategoryWrongCategory = wrongCategory,使得离线分析可以直接定位错误来源与纠正方向。时间戳统一采用 DateTime.UtcNow 以便跨时区一致地执行最近窗口抽取;FeedbackType = FeedbackType.UserCorrection 标记类型,为数据治理与报表分层提供维度。

持久化通过 _feedbackCollection.InsertOne(feedback) 完成,结合集合上的时间降序索引,可以快速检索最近的纠正样本用作“伪增量”训练窗口。通常服务编排层会在写入纠正后立即触发模型重训,因为纠正样本最能揭示当前模型的判错边界,及时吸收能显著降低连续错误带来的体验成本。若部署环境对写入幂等与一致性要求更高,可在外围增加去重键或重试带退避策略,避免网络抖动导致的重复写入。

Tip:纠正样本在 LTR 转换中会生成以该 IdGroupId 的多行训练数据,其中正确类目被标记为 Label=1,其他候选为 Label=0。这种带有明确负样本的成组数据,有助于排序模型学习到“应提升谁、应压制谁”,相较仅有正样本的选择数据更具梯度信息密度,因此在增量阶段往往会赋予更高的权重或更短的触发间隔,以加快模型在实际错误上的自修复速度。

2.4 索引与最近反馈窗口(GetRecentFeedbacks)

最近窗口是“伪增量”再训练的核心输入。我们围绕 Timestamp 建立降序索引,并配合 Limit(count) 以常数复杂度截取最新 N 条反馈,同时在 UserIdFeedbackType 上建立辅助索引,便于做用户/类型维度的监控与抽样。窗口大小与重训频率协同决定了新鲜度与稳定性的权衡:窗口越小越敏捷,越大越平滑,建议结合线上低置信度与纠正率曲线动态校准。代码如下:

public IEnumerable<UserFeedback> GetRecentFeedbacks(int count)
{return _feedbackCollection.Find(FilterDefinition<UserFeedback>.Empty).SortByDescending(f => f.Timestamp).Limit(count).ToList();
}

这段代码使用 Find(FilterDefinition<UserFeedback>.Empty) 获取集合中的全部文档作为查询起点,随后以 SortByDescending(f => f.Timestamp) 按时间戳降序排序,依赖我们预先建立的 Timestamp 降序索引以避免全量扫描。Limit(count) 将结果裁剪为最新的 N 条反馈,复杂度近似于按索引读取前 N 项,ToList() 则在方法边界把游标物化为内存列表,便于后续在训练层同步消费。由于时间戳统一为 UTC,跨时区部署时也能得到一致的“最近”语义;若需要限定时间范围或特定类型/用户的窗口,可以把空过滤替换为相应的 Builders<UserFeedback>.Filter 组合,例如在促活活动期间聚焦最近 24 小时且仅纠正样本的子窗口以提高增量的信噪比。

2.5 导出训练数据(ToTrainingData)

导出训练数据把持久化的用户反馈批量转换为 LTR 可训练样本,对每条反馈按当时候选生成多行“查询-候选”记录,使用反馈 Id 作为 GroupId 保持分组一致性,将用户最终选择标注为 Label=1、其余候选标注为 Label=0,并携带 UserId/Merchant/AmountBucket/HourOfDay 等上下文以与线上预测特征同构。如果候选列表缺失或未包含正确类目,会在转换时自动补齐,从而满足排序学习“每组至少一个正样本”的要求。这一转换既用于全量重训,也便于离线评估与回放。

public IEnumerable<LtrRow> ToTrainingData()
{var rows = new List<LtrRow>();var feedbacks = _feedbackCollection.Find(FilterDefinition<UserFeedback>.Empty).ToList();foreach (var feedback in feedbacks){var gid = feedback.Id;var candidates = feedback.AvailableCategories.Count > 0 ? feedback.AvailableCategories : new List<string> { feedback.SelectedCategory };if (!candidates.Contains(feedback.SelectedCategory)) candidates = new List<string>(candidates) { feedback.SelectedCategory };foreach (var candidate in candidates){var label = candidate == feedback.SelectedCategory ? 1f : 0f;rows.Add(new LtrRow{Query = feedback.Query,Candidate = candidate,Label = label,GroupId = gid,UserId = feedback.UserId,Merchant = feedback.Merchant,AmountBucket = feedback.AmountBucket,HourOfDay = feedback.HourOfDay});}}return rows;
}

上面的代码实现从 MongoDB 读取全部反馈到内存列表,随后在内存中完成样本化。这种做法在数据量中小规模阶段最为直接可靠,便于在训练前做一致性与完整性检查。在规模继续增长时可以按批分页读取或做流式消费以降低峰值内存占用。进入遍历后,首先以 feedback.Id 充当 gid,这是训练时 LTR 分组键的来源,保证同一次用户交互产生的多条“查询-候选”记录被排序学习算法视为同组,进而学习相对相关性。

候选集合的构造遵循“原样还原、缺失补齐”的原则:优先使用持久化的 AvailableCategories,如果为空则退化为仅包含 SelectedCategory 的最小集合,之后再检测是否包含正确类目,如果未包含则显式追加,确保每个分组内至少有一个正样本。之所以把补齐放在样本化阶段,是为了最大化保留当时用户所见候选的真实分布,同时避免因为上游传参疏漏而让 LTR 落入只有负样本或无正样本的非法状态。

样本行的生成在内层循环完成:对每个 candidate 创建一条 LtrRow,其中 Query 复用原始消费描述,Candidate 为该候选类目名,Label 采用“选中为 1,其它为 0”的二值标注,GroupId 统一设置为前述的 gidUserIdMerchantAmountBucketHourOfDay 则将上下文特征一并带入,以确保训练与线上预测使用同构的特征空间。最终以平铺列表的形式返回,供上层训练流程加载为 IDataView,并在管道中通过 MapValueToKey("group_key", nameof(LtrRow.GroupId)) 把字符串分组键映射成 LTR 训练所需的 Key 类型。

从工程角度看,该实现依赖若干前置不变式,SelectedCategory 不为空、Query 合法且已做必要的清洗、AvailableCategories 内部不包含明显重复。如果存在长尾用户拥有极多候选的情形,内层“全量候选转样本”的复杂度会线性上升,这时可以在样本化前对候选做去重或轻量负采样以控制训练规模,同时保持至少一个正样本不变。对于极大数据集,读取阶段可以增加投影只取必需字段,或在数据库端按 Timestamp 做时间窗与分页,配合批量训练以降低单次内存压力。

三、触发再训练:频率、窗口与“伪增量”

3.1 触发条件与节奏(TriggerIncrementalLearningIfNeeded)

再训练的触发来源于两条路径:其一是按反馈计数的周期触发(每累计 N 条反馈触发一次),其二是高价值的“纠正”反馈即时触发。前者保证稳定节奏与成本可控,后者用于快速修复最新的判错模式。

private void TriggerIncrementalLearningIfNeeded()
{if (_feedbackStorage.Count % _options.RetrainingFrequency == 0){TriggerIncrementalLearning();}
}

TriggerIncrementalLearningIfNeeded 用于按“固定步长”触发重训:当累计反馈数 _feedbackStorage.Count 能被配置的 RetrainingFrequency 整除(例如每满 5 条),就调用 TriggerIncrementalLearning()。好处是节奏稳定、成本可控,频率改由 ProgressiveLearningOptions 配置,便于按环境调整。

3.2 重训流程(TriggerIncrementalLearning)

重训流程负责在满足最小样本量的前提下执行一次完整的模型更新。冷启动阶段进行首次全量训练。在进入稳定期后采用“伪增量”策略,先取最近窗口做样本化,再与历史样本合并重训。训练完成后基于训练数据的 schema 持久化模型文件,便于下次加载与灰度回滚,同时对异常进行捕获以避免打断线上流程。

public void TriggerIncrementalLearning()
{try{var trainingData = _feedbackStorage.ToTrainingData().ToList();if (trainingData.Count < _options.MinTrainingDataSize) return;if (_matcher.TrainingDataCount == 0){_matcher.Train(trainingData);}else{var recentFeedbacks = _feedbackStorage.GetRecentFeedbacks(_options.IncrementalBatchSize);var incrementalData = ConvertFeedbacksToTrainingData(recentFeedbacks).ToList();if (incrementalData.Count > 0) _matcher.AddTrainingData(incrementalData);}var schemaSource = MLContextSingleton.GetSchemaFromEnumerable(trainingData);_matcher.SaveModel(_modelPath, schemaSource);}catch (Exception ex){Console.WriteLine($"Incremental learning failed: {ex.Message}");}
}

方法开头通过 _feedbackStorage.ToTrainingData().ToList() 拉取并物化当前全量训练数据,用作最小样本量闸门与后续持久化的 schema 来源。if (trainingData.Count < _options.MinTrainingDataSize) return; 这一步可以避免在数据稀少、标签分布尚不稳定时触发一次性成本较高且可能过拟合的训练过程,保证训练仅在“值得”的情况下发生。

随后根据 _matcher.TrainingDataCount 判断是否为冷启动。如果尚无已训练模型,则直接对 trainingData 做全量训练,确保最快得到一个可用的初始模型。如果已存在模型,则走“伪增量”路径:先用 _feedbackStorage.GetRecentFeedbacks(_options.IncrementalBatchSize) 获取最近窗口,再经 ConvertFeedbacksToTrainingData 转换为 LTR 行,得到 incrementalData 后调用 _matcher.AddTrainingData(incrementalData)。虽然命名为“增量”,但实现是将新样本加入历史样本集合并重新训练整个管道,从而在保证一致性的同时简化真正在线增量学习的复杂度与风险。

训练完成后,使用 MLContextSingleton.GetSchemaFromEnumerable(trainingData) 从全量训练数据推导 schema,并以 _matcher.SaveModel(_modelPath, schemaSource) 持久化模型。保存时带上 schema 的原因是确保后续加载阶段的列名、类型、向量维度与训练时完全一致,避免因特征工程变化或列顺序差异导致的加载/推理错误。整个流程被包裹在 try/catch 中,任何异常都会被捕获并记录到日志,以免重训失败扩散为 API 级别的故障,失败时保留旧模型继续服务,配合上层“规则回退”分支能够保持线上可用性。

3.3 最近窗口样本化(ConvertFeedbacksToTrainingData)

为保持 LTR 分组一致性与正负样本齐备,最近窗口内的反馈会被转换为“查询-候选”多行样本,每条反馈的 Id 作为 GroupId,并在候选缺失时自动补齐正确类目。样本化遵循“原样还原、缺失补齐”的原则:优先还原当时可见候选,若缺失或未含正确类目则在转换时补齐;同时携带 UserId/Merchant/AmountBucket/HourOfDay 等上下文,保持与线上预测同构的特征。这样生成的窗口样本可与历史样本合并用于“伪增量”重训,既快速吸收最新信号,又避免在线增量的复杂性与不确定性。代码如下:

private IEnumerable<LtrRow> ConvertFeedbacksToTrainingData(IEnumerable<UserFeedback> feedbacks)
{var rows = new List<LtrRow>();foreach (var feedback in feedbacks){var gid = feedback.Id;var candidates = feedback.AvailableCategories.Count > 0 ? feedback.AvailableCategories : new List<string> { feedback.SelectedCategory };if (!candidates.Contains(feedback.SelectedCategory)) candidates = new List<string>(candidates) { feedback.SelectedCategory };foreach (var candidate in candidates){var label = candidate == feedback.SelectedCategory ? 1f : 0f;rows.Add(new LtrRow{Query = feedback.Query,Candidate = candidate,Label = label,GroupId = gid,UserId = feedback.UserId,Merchant = feedback.Merchant,AmountBucket = feedback.AmountBucket,HourOfDay = feedback.HourOfDay});}}return rows;
}

函数接收最近窗口的反馈枚举后,在内存中构造扁平的 LTR 训练行列表。首先以 feedback.Id 作为 gid,该值直接充当 LTR 的分组键,确保同一次交互产生的多条“查询-候选”记录被排序学习算法视为同组,从而学习到相对相关性而非独立的二分类信号。

候选集合优先还原持久化时记录的 AvailableCategories,如果为空则退化为仅包含 SelectedCategory 的最小集合,随后再次检查并在缺失时显式追加正确类目,保证每个分组里至少存在一个正样本。把补齐放在样本化阶段而不是写入阶段,有助于最大限度保留当时用户可见候选的真实分布,同时避免上游传参不全导致的“无正样本”非法分组。

对于每个候选类目,生成一条 LtrRowQuery 直接复用反馈中的原始文本,Candidate 为候选类目名,Label 采用“正确为 1、其余为 0”的二值标注,GroupId 统一设置为 gid。同时把 UserIdMerchantAmountBucketHourOfDay 等上下文特征一并写入,保证训练端与线上预测端的特征空间同构,避免特征漂移造成的线上/线下不一致。最终返回的扁平列表可被上层训练流程加载为 IDataView,并在管道中通过 MapValueToKey 把字符串分组键映射为 LTR 训练要求的 Key 类型。

这种实现简单直观,便于在窗口较小的场景下快速吸收最新信号。当遇到拥有极多候选的长尾用户时,可以在进入内层循环前做去重或轻量负采样以控制样本规模,同时保持至少一个正样本不变,从而在训练效率与样本充分性之间取得更好的平衡。

3.4 实践要点

“伪增量”(全量数据 + 近期窗口重训)的核心价值在于工程简洁与质量可控。它不依赖在线渐进式算法的内部状态持久化与顺序一致性,训练路径天然可重放、可审计,便于在多环境下复现结果并做灰度回滚。配合最小样本量闸门与固定频率触发,可以在小中规模阶段以极低的实现复杂度获得稳定收益,并用模型文件的版本化管理保障发布与回退的确定性。

当数据规模提升并出现更强的概念漂移或季节性波动时,建议采用“滚动窗口 + 周期全量校准”的组合。日常以较小的时间窗快速吸收最新反馈,保持模型对近期语义的敏感性,按周或按月执行一次全量重训,对长期记忆与特征分布进行校准,纠正窗口策略可能带来的偏向。窗口大小与触发频率应结合线上低置信度占比、纠正率与评估指标(如 NDCG/ MAP)滑窗曲线动态调整,并对每次重训保留评估报告与 schema 快照,便于后续排障与对比分析。

四、配置与依赖注入

4.1 服务注册与依赖注入(Program.cs)

通过 AddSingletonProgressiveLearningManager 注册到容器中,启动时从配置读取模型路径与 MongoDB 连接,并构造 ProgressiveLearningOptions 注入到实例。注入代码如下:

builder.Services.AddSingleton<ProgressiveLearningManager>(provider =>
{var config = provider.GetRequiredService<IConfiguration>();var modelPath = config["ML:ModelPath"] ?? Path.Combine(AppContext.BaseDirectory, "Models", "category_model.zip");var connectionString = config["ML:MongoDB:ConnectionString"] ?? "mongodb://admin:admin@14.103.224.141:27017/admin";var databaseName = config["ML:MongoDB:DatabaseName"] ?? "SporeAccountingML";var collectionName = config["ML:MongoDB:FeedbackCollectionName"] ?? "UserFeedbacks";var options = new ProgressiveLearningOptions{ConfidenceThreshold = config.GetValue<float>("ML:ConfidenceThreshold", 0.4f),MinTrainingDataSize = config.GetValue<int>("ML:MinTrainingDataSize", 10),RetrainingFrequency = config.GetValue<int>("ML:RetrainingFrequency", 5),IncrementalBatchSize = config.GetValue<int>("ML:IncrementalBatchSize", 10)};return new ProgressiveLearningManager(modelPath, connectionString, databaseName, collectionName, options);
});

这里的依赖注入做了三件事:解析配置、构造选项、注册单例服务。首先通过 var config = provider.GetRequiredService<IConfiguration>(); 取到配置根,随后读取关键项并提供健壮的默认值。例如 modelPath = config["ML:ModelPath"] ?? Path.Combine(AppContext.BaseDirectory, "Models", "category_model.zip") 会优先使用配置中的路径,否则落到应用目录下的 Models/category_model.zipAppContext.BaseDirectory 确保发布后路径可用。connectionString/databaseName/collectionName 同理来自 ML:MongoDB:*,缺失时使用默认(生产建议迁移至环境变量或密钥库以避免明文)。options = new ProgressiveLearningOptions { … } 通过 GetValue<T>(key, default) 读取阈值与阈门,配置缺失则回退到默认,这四个参数分别控制人机协同阈值、最小样本量、重训频率与增量窗口大小,与上文训练/再训练流程一一对应。

随后 AddSingleton<ProgressiveLearningManager>(provider => …) 返回一个已装配好的实例,即 new ProgressiveLearningManager(modelPath, connectionString, databaseName, collectionName, options) 将模型路径、Mongo 参数与训练选项一次性注入;构造函数内部会尝试加载已有模型并初始化反馈存储与匹配器。选择 Singleton 的原因是训练与模型状态在进程内共享,单例避免模型多份拷贝与状态不一致,配合线程安全的 ML.NET 推理与 Mongo 驱动连接池,适合 Web API 常驻进程场景。

配套的 appsettings.json 片段定义了同名键,ML.ModelPath 指向模型文件,ML.MongoDB.* 提供数据库连接信息;ML.ConfidenceThreshold/MinTrainingDataSize/RetrainingFrequency/IncrementalBatchSize 分别对应预测阈值、最小训练样本量、按反馈条数触发重训的步长与最近反馈窗口大小,它们被 GetValue<T> 读取并可按环境覆盖(开发/测试/生产)。

4.2 配置项与默认值(appsettings.json)

核心阈值、最小样本量、重训频率与增量窗口大小等都由配置项驱动,可按环境差异化覆盖默认值。配置项如下:

"ML": {"ModelPath": "Models/category_model.zip","MongoDB": {"ConnectionString": "mongodb://admin:admin@14.103.224.141:27017/admin","DatabaseName": "SporeAccountingML","FeedbackCollectionName": "UserFeedbacks"},"ConfidenceThreshold": 0.4,"MinTrainingDataSize": 10,"RetrainingFrequency": 5,"IncrementalBatchSize": 10
}

这些配置项分别控制模型文件位置、反馈存储与再训练节奏。其中 ModelPath 指定模型文件的读写路径。未配置时默认落在应用目录的 Models/category_model.zip,用于启动加载与训练后保存。MongoDB.ConnectionString/DatabaseName/FeedbackCollectionName 定义反馈数据的持久化位置与集合名,用于记录选择/纠正并导出训练样本。生产环境建议改为环境变量或密钥库,账号仅授最小权限,并确保对 Timestamp/UserId/FeedbackType 建好索引。ConfidenceThreshold 是预测置信度门槛,高于阈值可自动分类,低于阈值在响应中标记 RequiresUserConfirmation=true 交由前端确认。数值越高越保守,交互更多但更稳。MinTrainingDataSize 是最小样本量闸门,样本不足时直接跳过重训,避免冷启动阶段因数据稀少带来的过拟合与不稳定。RetrainingFrequency 是“按条数”触发的固定步长,每累计 N 条反馈触发一次重训,保证节奏与成本可控。IncrementalBatchSize 是最近反馈窗口大小,配合“伪增量”策略使用:从最近窗口取样本与历史合并重训。窗越小越敏捷、越大越平滑,建议与频率联动调优。

五、API 契约与时序

5.1 预测(POST /api/CategoryPrediction/predict)

预测功能接收 Query 与用户全部类目列表,调用 SmartPredict 自动选择 ML/规则路径,返回推荐类目、置信度以及是否需要用户确认。

[HttpPost("predict")]
public async Task<ActionResult<PredictionResponse>> Predict([FromBody] PredictionRequest request)
{var categories = request.Categories.Select(c => new UserCategory(c.Id, c.Name)).ToList();var prediction = _learningManager.SmartPredict(request.Query, categories, request.UserId, request.Merchant, request.AmountBucket, request.HourOfDay);return Ok(new PredictionResponse{PredictedCategory = new CategoryDto { Id = prediction.PredictedCategory.Id, Name = prediction.PredictedCategory.Name },Confidence = prediction.Confidence,Method = prediction.Method.ToString(),RequiresUserConfirmation = prediction.RequiresUserConfirmation});
}

Action从请求体中接收 PredictionRequest,首先将传入的 Categories 投影为领域模型 UserCategory 并物化为列表,以确保后续预测阶段对候选的枚举是稳定的。随后调用 _learningManager.SmartPredict,把 Query 与候选列表连同可选的 UserIdMerchantAmountBucketHourOfDay 一并传入。这些上下文特征在排序模型中分别作为文本/类别/数值特征参与打分,用于提升个性化与情境判断。

SmartPredict 内部会根据当前训练状态与阈值自动选择 ML 排序或规则回退,并在 ML 路径下依据置信度阈值计算是否需要用户确认。该方法返回的 SmartPredictionResult 包含最终推荐类目、置信度、采用的方法以及是否需要确认的标志。控制器将其映射为对外的 PredictionResponse:把领域内的 UserCategory 转为 CategoryDto,并原样回传 ConfidenceMethodRequiresUserConfirmation。前端据此决定是直接采用结果,还是弹出确认/更改交互,以便把选择或纠正反馈回系统供后续再训练使用。

5.2 记录选择(POST /api/CategoryPrediction/feedback/choice)

记录选择将用户对候选的正反馈持久化,作为正样本用于后续训练,代码如下:

[HttpPost("feedback/choice")]
public async Task<ActionResult> RecordChoice([FromBody] ChoiceFeedbackRequest request)
{var selectedCategory = new UserCategory(request.SelectedCategory.Id, request.SelectedCategory.Name);var availableCategories = request.AvailableCategories.Select(c => new UserCategory(c.Id, c.Name));_learningManager.RecordUserChoice(request.Query, selectedCategory, availableCategories, request.UserId, request.Merchant, request.AmountBucket, request.HourOfDay);return Ok(new { message = "Choice recorded successfully" });
}

这个Action从 ChoiceFeedbackRequest 中读取用户的原始查询、所选类目与当时可见的候选集合,并将传输对象投影为领域模型 UserCategory,这样一方面保证后续服务层使用统一的领域类型,另一方面将候选立即物化为稳定序列,避免延迟枚举带来的不确定性。随后把 Query、所选类目、候选集合,以及 UserId/Merchant/AmountBucket/HourOfDay 等上下文一并交给学习管理器,由服务层完成反馈落库与后续增量学习的触发判定。

该接口本身是一个轻量的写入端点,返回语义化的成功消息即可。是否立即重训由服务层依据配置的 RetrainingFrequency 决定,从而在数据新鲜度与资源成本之间取得平衡。而持久化的选择反馈会在样本转换阶段生成包含正样本的 LTR 训练行,持续提升后续预测质量。

5.3 记录纠正(POST /api/CategoryPrediction/feedback/correction)

记录纠正用于记录模型错误与用户正确答案的并置样本,通常触发即时重训。代码如下:

[HttpPost("feedback/correction")]
public async Task<ActionResult> RecordCorrection([FromBody] CorrectionFeedbackRequest request)
{var wrongCategory = new UserCategory(request.WrongCategory.Id, request.WrongCategory.Name);var correctCategory = new UserCategory(request.CorrectCategory.Id, request.CorrectCategory.Name);var availableCategories = request.AvailableCategories.Select(c => new UserCategory(c.Id, c.Name));_learningManager.RecordUserCorrection(request.Query, wrongCategory, correctCategory, availableCategories, request.UserId, request.Merchant, request.AmountBucket, request.HourOfDay);return Ok(new { message = "Correction recorded successfully" });
}

这个Action处理的是“模型给错—用户改正”的闭环场景。它从 CorrectionFeedbackRequest 读取三类核心信息:模型当时的 WrongCategory、用户认为正确的 CorrectCategory,以及当时界面展示给用户的 AvailableCategories 候选集。首先将这三者统一投影为领域模型 UserCategory,确保跨层调用的类型一致与语义清晰,同时将候选序列在控制器侧即刻物化,避免延迟枚举造成的数据漂移。

完成投影后,控制器把 Query 与用户上下文(UserId/Merchant/AmountBucket/HourOfDay)与上述类目信息一并交给学习管理器。服务层会将这次纠正作为一条显式的“并置样本”入库,其中 SelectedCategory = CorrectCategory 形成正样本,而 WrongCategory 则用于在样本转换阶段生成明确的负样本,从而在 LTR 训练集中构造同一组内的对比对,强化模型的判别边界并缩短收敛路径。端点本身只需返回一次语义化成功消息;是否立即触发重训由服务层依据策略与 RetrainingFrequency 决定,但纠正类反馈通常被赋予更高的训练优先级,以尽快修复用户可感知的错误。

5.4 手动重训(POST /api/CategoryPrediction/retrain)

手动重训用于绕过自动频率门限,立即触发一次受控的“伪增量”流程,常见于批量导入历史流水后或观察到质量回落时的人工干预。该端点不需要请求体,直接调用学习管理器的重训方法并返回一次语义化确认。控制器层保持幂等与无副作用,真正的训练逻辑封装在服务层,以便统一治理日志、异常和模型落盘。

[HttpPost("retrain")]
public async Task<ActionResult> TriggerRetraining()
{_learningManager.TriggerIncrementalLearning();return Ok(new { message = "Retraining triggered successfully" });
}

服务层在收到触发后,会拉取完整或最近窗口的反馈,先根据最小样本阈值进行保护,避免在样本稀疏时训练出不稳定模型。若当前尚无模型,则执行一次全量首训;若已有模型,则将最近窗口反馈转换为 LTR 行为样本后追加到训练集中,完成“伪增量”追加,再进行模型保存。保存时以当前训练数据的 Schema 为基准落盘,保证下次加载推理与训练一致。整个过程中的异常被吞吐并记录到日志,以确保 API 层的可用性不受短时故障影响。

由于它是主动触发入口,建议仅在有明确理由需要“立刻刷新”的场景调用,例如完成一次较大批次的数据修复后。日常运行仍以自动频率为主,以减少频繁更新带来的波动与回归风险。

5.5 学习统计(GET /api/CategoryPrediction/stats)

学习统计提供一个面向运维与产品的轻量可观测性窗口,帮助判断“是否需要干预”和“模型是否在持续学习”。该端点聚合了反馈与训练状态的关键指标,便于前端仪表盘或告警系统直接消费。

[HttpGet("stats")]
public async Task<ActionResult<LearningStatsResponse>> GetStats()
{var stats = _learningManager.GetStats();return Ok(new LearningStatsResponse{TotalFeedbacks = stats.TotalFeedbacks,TrainingDataSize = stats.TrainingDataSize,ModelExists = stats.ModelExists,RecentFeedbacks = stats.RecentFeedbacks});
}

返回对象中的 TotalFeedbacks 反映 MongoDB 中累计反馈量,是数据新鲜度与用户参与度的直接代理;TrainingDataSize 报告当前用于训练或可用于训练的样本行数,可间接评估模型的容量与覆盖;ModelExists 用于快速判断线上是否存在可加载的模型文件,便于在冷启动或回滚后确认可用态;RecentFeedbacks 统计最近窗口内的反馈条数,为自动重训频率与触发阈值提供依据。通过这些指标,可以在无需深入日志的情况下,快速定位问题:若反馈增长而训练规模滞后,说明重训节奏可能偏慢;若模型不存在但反馈持续增加,应尽快首训并落盘。

该接口本身不执行业务变更,因此调用安全且低成本。建议把它接入到可视化面板,并结合阈值配置告警,以便在数据分布突变或用户纠错飙升时及时干预。

六、冷启动与回滚策略

在冷启动阶段,优先以规则回退兜底,避免尚未成型的模型产生误导;同时把纠正型反馈作为高价值信号优先采集与训练,以最小样本快速构建第一版可用模型。若存在历史流水或标注数据,可以在导入时即时触发一次手动重训,完成从“无模型”到“弱模型”的快速跃迁;在导入与首训的全过程中,务必保持与线上推理一致的特征工程与 Schema,避免首训后出现特征漂移。

在回滚策略上,建议为模型文件建立版本化命名与原子落盘(先写临时名再切换)机制,至少保留最近两个稳定版本。当线上出现离群预测、服务异常或离线评测显著回退时,直接回退到上一个稳定版本,同时 API 层继续保留规则分支以保障可用性。配合统计接口与低门槛的手动重训入口,可以在“发现问题—回退—修复—再发布”的闭环中快速收敛,减少对用户的可感知影响。

七、总结

本文构建了一个可在生产环境落地的“渐进式学习”闭环。线上由 SmartPredict 在 ML 排序与四层规则间自适应切换,用置信度阈值把自动化决策转化为人机协同,线下把“选择/纠正”反馈连同候选与上下文沉淀到 MongoDB,并在样本化阶段生成分组一致、正负齐备的 LTR 训练对,训练侧采用“历史全量 + 最近窗口”的伪增量策略,受最小样本与固定频率门控并在成功后原子落盘,所有能力通过统一 API 与 DI 配置对外暴露,保障了可解释、可调参与可回滚。

数据与训练的关键在于特征同构与节奏治理。训练与推理共享 Query/UserId/Merchant/AmountBucket/HourOfDay 的特征空间以避免漂移;MinTrainingDataSize 防止早期过拟合,RetrainingFrequencyIncrementalBatchSize 决定新鲜度与稳定性的折中;纠正样本优先进入最近窗口以加速修复。配合 stats 接口监控低置信度占比、纠正率与排名指标(如 NDCG/MAP),并结合模型文件的版本化与原子落盘、规则回退兜底和手动重训入口,形成“发现—回退—修复—再发布”的工程化闭环。

落地与演进建议是先小步快跑、后体系化优化。短期围绕阈值、窗口与规则库做灰度调参,确保稳定收益,中期引入特征扩展(如商户 embedding、子词/拼写归一)、规则外置与 A/B 实验、在线评估与回放管线、多租户隔离与配额,长期可探索真增量/蒸馏与周期全量校准的组合,并完善隐私合规与数据生命周期治理。在既有架构上,这些改造都可作为可插拔扩展逐步推进,而无需推翻现有闭环。

http://www.dtcms.com/a/473910.html

相关文章:

  • 2.c++面向对象(五)
  • python中的一些运算符
  • 【嵌入式面试题】boss收集的11道,持续更新中
  • 保证样式稿高度还原
  • 网站建设 源码怎么注册公司名
  • [xboard] 34 buildroot 的overlay机制
  • 某公司站点的挖掘实战分享
  • 第三方和审核场景回调还是主动查询
  • Git基本命令的使用(超详细)
  • NC40 链表相加(二)
  • 网安面试题收集(3)
  • JetLinks设备接入的认识与理解
  • 从HashMap到ConcurrentHashMap深入剖析Java并发容器的演进与实战
  • 做一组静态页面网站多少钱网站源码上传到哪个文件夹
  • 威海市城乡建设局网站网络整合营销服务商
  • 从报头到路由器——【网络编程】详解 IP 协议:报头字段、路由器功能、网段划分和分片传输
  • 网站验证北京建网站开发
  • 设计模式篇之 装饰器模式 Decorator
  • 虚幻引擎虚拟制片入门教程 之 创建项目及启用插件
  • 淳安县建设网站王璞网站开发实战答案
  • Linux禁用自带键盘和触摸板(无需每次开机重置)
  • 149、【OS】【Nuttx】【周边】效果呈现方案解析:VSCode 打开外部链接(二)
  • Apache Commons IO:文件流处理利器,让Java IO操作更简单
  • 哪个网站做简历免费自己做免费网站
  • 医院预约挂号|基于Java+vue的医院预约挂号系统小程序的设计与实现(源码+数据库+文档)
  • 翻转二叉树---超全详细解
  • AI智能体全球应用调查报告:从“对话”到“做事”的变革
  • Linux网络之----网络编程
  • [Power BI] CALCULATETABLE函数
  • 3494. 酿造药水需要的最少总时间