自动分类不是玄学:Paperless-ngx 的机器学习文档归类机制全揭秘
自动分类不是玄学Paperless-ngx 的机器学习文档归类机制全揭秘【免费下载链接】paperless-ngxA community-supported supercharged document management system: scan, index and archive all your documents项目地址: https://gitcode.com/GitHub_Trending/pa/paperless-ngx很多人第一次听说 Paperless-ngx是被它自动分类文档的宣传吸引扫描进来的账单、合同、发票它自己就知道该打什么标签、归到哪个发件人correspondent、算哪种文档类型。社区里流传的部署教程也总是把它当作开箱即用的文档管家。但真正用起来你会发现自动分类的准确率忽高忽低——有时候神准有时候完全不着调。这背后的原因恰恰藏在这套机制的真实工作原理里它既不是AI 玄学也不是简单的关键词规则而是一个由规则匹配 神经网络分类器 周期性再训练构成的混合系统。本文直接拆开 src/documents/classifier.py 和 src/documents/matching.py 的源码讲清楚三件事它到底怎么从你的历史标注里学习规则匹配和机器学习预测之间谁说了算以及为什么训练数据不够时分类就是不准。先分清两套系统规则匹配 vs 机器学习预测打开 src/documents/models.py在MatchingModel这个抽象基类里可以找到 7 种匹配算法的枚举MATCH_NONE 0 MATCH_ANY 1 MATCH_ALL 2 MATCH_LITERAL 3 MATCH_REGEX 4 MATCH_FUZZY 5 MATCH_AUTO 6前 6 种是纯规则匹配Any任意关键词命中、All全部关键词命中、Exact精确字串、Regular expression正则、Fuzzy word模糊匹配实现都在 src/documents/matching.py 的matches()函数里。只有第 6 种MATCH_AUTO才是机器学习。注意一个容易误会的点在匹配流程里MATCH_AUTO在matches()中直接返回False源码注释写着 this is done elsewhere——规则匹配器不处理它Auto 的决策完全交给分类器预测两条路径是解耦的。这也解释了为什么一个标签设置成 Auto 之后界面上匹配文本一栏留空也没关系它本来就不靠关键词。分类器是怎么学习的特征、标签、神经网络自动匹配的引擎是 src/documents/classifier.py 里的DocumentClassifier。整个学习管线分三步第一步从数据库捞训练样本。注意 src/documents/classifier.py 的train()方法开头的过滤条件docs_queryset Document.objects.exclude( tags__is_inbox_tagTrue, ).order_by(pk)带 inbox 标签的文档被直接排除在训练集之外。这是刻意的设计inbox 里的文档通常还没来得及人工确认分类属于脏数据不配当老师的教材。官方文档 docs/advanced_usage.md 也明确写了这条约束确保神经网络只从你已经确认归类正确的文档里学。第二步提取标签。对每一篇文档只统计那些matching_algorithm MATCH_AUTO的标签、correspondent、文档类型和存储路径作为监督信号y -1 dt doc.document_type if dt and dt.matching_algorithm MatchingModel.MATCH_AUTO: y dt.pk值为-1表示这篇文档没有自动类目它照样进训练集——因为负样本和正样本同样重要分类器需要学会什么时候不归给任何类。第三步向量化 训练。文本内容先经preprocess_content()做归一化、去停用词、词干化stemming然后交给CountVectorizer生成词频特征ngram_range(1, 2)即同时看单词和二元词组min_df0.01过滤掉出现频率低于 1% 的稀有词self.data_vectorizer CountVectorizer( analyzerword, ngram_range(1, 2), min_df0.01, )特征矩阵分别喂给 4 个独立的MLPClassifier多层感知机神经网络分别负责 tags、correspondent、document_type、storage_path 四类预测。每个类目各学各的互不干扰。还有一个容易被忽略的细节correspondent 和 document_type 的训练都传了sample_weightcompute_sample_weight(balanced, ...)。注释里写得很明白——MLPClassifier不支持class_weight所以用样本权重来平衡类别防止出现频率高的发件人垄断预测结果。比如你 90% 的文档都来自同一家银行不平衡的话分类器会倾向把任何新文档都判给这家银行。预测阶段置信度阈值这道防呆闸门模型训练完保存为classification_model.pickle默认路径DATA_DIR / classification_model.pickle预测时通过_predict_with_threshold把关probas classifier.predict_proba(X)[0] best_idx int(probas.argmax()) best_class int(classifier.classes_[best_idx]) if best_class -1: return None if threshold 0.0 and probas[best_idx] threshold: return None return best_class它不用predict()的硬性输出而是取predict_proba()里最高概率的类别然后跟阈值比最高置信度低于阈值直接返回 None宁可不管也不瞎归。这个阈值默认是 0.3src/paperless/settings/init.pyCLASSIFIER_MATCH_THRESHOLD可用环境变量PAPERLESS_CLASSIFIER_MATCH_THRESHOLD覆盖。这个设计直接解释了日常使用中两个常见现象为什么文档太少时什么都不自动归——样本少模型置信度上不去全部被阈值拦下为什么调低阈值后什么都乱归——阈值一放松低置信度的猜测也会被接受。规则与机器学习如何协作优先级与冲突处理真正有意思的是规则匹配和 Auto 预测叠加而不是互斥。以 src/documents/matching.py 的match_correspondents()为例return list( filter( lambda o: ( matches(o, document) or ( o.pk pred_id and o.matching_algorithm MatchingModel.MATCH_AUTO ) ), correspondents, ), )一句话一个 correspondent 只要能命中规则matches或者被分类器预测到Auto就算匹配成功。所以一个关键字匹配 Auto混用的库是常态银行账单既可能因为规则里写了 Bank of America 被匹配也可能因为神经网络认出了内容特征而被 Auto 归给同一个类目。冲突时谁赢看 src/documents/signals/handlers.py 的set_correspondent()potential_correspondents matching.match_correspondents(document, classifier) potential_count len(potential_correspondents) selected potential_correspondents[0] if potential_correspondents else None if potential_count 1: if use_first: # 取第一个 else: # 多个候选时干脆不指派返回 None默认use_firstTrue多个候选命中时取列表第一个列表按名称排序但文档也给了保守模式——use_firstFalse时只要出现多个候选就直接放弃指派宁缺毋滥。document_retagger管理命令src/documents/management/commands/document_retagger.py提供--use-first开关默认行为就是不指派避免批量重算时误伤存量文档。再训练机制为什么改动不会立刻生效社区反馈里最常见的困惑是我改了标签规则为什么新文档还是按旧规则分类答案在训练触发机制里。train_classifier任务src/documents/tasks.py由调度器周期性执行默认每小时检查一次但检查不等于训练。train()内部有一套变了才重训的判定它记录上次训练时的最新文档修改时间last_doc_change_time和所有 Auto 类目标签的主键哈希last_auto_type_hash只有当二者之一发生变化时才真正重新拟合模型if ( self.last_doc_change_time is not None and self.last_doc_change_time latest_doc_change ) and self.last_auto_type_hash hasher.digest(): logger.info(No updates since last training) return False对应测试 src/documents/tests/test_classifier.py 里的test_no_retrain_if_no_change/test_retrain_if_change/test_retrain_if_auto_match_set_changed三个用例行为非常明确改文档元数据会触发重训把某个标签从 Any 改成 Auto 也会触发重训什么都不动就不重训。所以改完规则没反应往往不是 bug而是还没到下一个调度周期或者改动本身没改变哈希。想立即生效可以手动触发训练任务或运行相关管理命令。训练数据积累到什么程度分类才准这是大纲里最值得回答的问题而答案藏在源码和官方文档两处。src/documents/classifier.py 的train()里有几个硬性门槛一篇 Auto 类目都没有时直接跳过src/documents/tasks.py 中train_classifier开头检查四个类目是否至少有一个 Auto所有文档都在 inbox 里时抛ValueError(No training data available.)。更关键的数据量感知来自文档 docs/advanced_usage.md 对 Auto 算法的三条明确警告翻译成工程语言就是每个类目至少要有合理数量的正样本。文档举的例子很直白一千篇文档里只有一篇属于五年前买过东西的冷门网店就算你再下单它大概率也认不出来。原因是min_df0.01过滤 神经网络置信度阈值双重作用下稀有类目既拿不到足够特征词也攒不出高于 0.3 的置信度。必须积累足够的负样本。如果整个库里只有 Webshop 和 Bank 两个 Auto 类目且比例失衡分类器会把任何新文档都硬塞给其中一类——这正是compute_sample_weight(balanced, ...)想缓解但无法根治的问题它只能平衡类别权重不能凭空创造其他类的概念。类目与内容必须存在可学习的相关性。标签如果是 TODO 这种与文档正文无关的元状态模型永远学不会这不是训练量能解决的。多少篇才算够没有固定数字但可以从实现反推一个经验区间由于每个 Auto 类目对应一个独立的 MLP 输出节点而输入特征是全文词频向量每个类目至少需要几十篇带该标签的文档才能让特征词稳定出现min_df0.01意味着词至少出现在 1% 的文档中并且需要大体量的负样本把不属于任何类的概率压下去。这也是为什么 Paperless 官方建议用户先手动整理一段时间的文档、让类目样本自然积累而不是一上来就指望全自动。结论把自动分类当助手而非上帝从源码看Paperless-ngx 的自动分类是一个精心设计的工程系统规则匹配负责稳定、可解释的硬逻辑神经网络负责从历史标注中归纳软规律置信度阈值兜底防误判哈希比对保证只在数据真正变化时重训。它刻意规避了三个常见陷阱——用负样本避免什么都归一类、用样本权重避免大类别霸权、用阈值避免低置信度硬归。理解了这套机制你对它的预期就能校准先喂数据再谈准确率规则能写清楚的就写规则精确、可解释、即时生效规则写不清楚的才交给 Auto 慢慢学而分类不准的绝大多数排查路径都可以回到一句话——检查训练集是否够多、够均衡、够干净没有 inbox 脏数据。自动分类从来不是玄学它只是一门需要正确使用姿势的数据科学。【免费下载链接】paperless-ngxA community-supported supercharged document management system: scan, index and archive all your documents项目地址: https://gitcode.com/GitHub_Trending/pa/paperless-ngx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考