机器学习文本分类实战:打造本地笔记自动归档器

发布时间:2026/10/17 12:05:34
机器学习文本分类实战:打造本地笔记自动归档器 这个系列写到第十篇机器学习理论聊过不少但真正让我觉得“值回票价”的往往不是什么高深的模型而是把算法塞进日常流程里的小工具。今天这篇我打算记录一个刚完工的本地笔记自动归档器它会在你按下保存的那一刻根据笔记内容打上“工作”“灵感”“备忘”“读书”这类标签省去手动整理文件夹的功夫。项目本身不复杂——分词、向量化、一个线性分类器再加一个几行的文件监听脚本。但正是因为足够贴近日常它踩过的坑比前九篇加起来都多。如果你也攒了一堆乱七八糟的文本文件或者正想找一个适合自己的机器学习入门案例这篇可以直接参考。1. 项目需求与技术选型思路1.1 为什么想给笔记做自动标签每天产生大量笔记最麻烦的不是写下来而是整理。我自己的习惯是把所有想法、会议记录、读书摘抄全部塞进同一个 Markdown 文件夹时间一长文件名基本就是“未命名-副本-3”这种样子。想找一个月前写的东西只能全文搜索关键词。手动归档确实能解决一部分问题但每存一条笔记都得停下来想“这属于哪一类”一旦积累超过上千条整理本身就成了巨大的心理负担。尝试过用关键词规则来归类比如把包含“会议”“项目”“截止日期”的笔记放进工作类但现实中的文本不会那么听话很多工作笔记里也会提到做饭和跑步。真正的区分信号往往藏在上下文里而不是某个孤立的词。就拿我自己的一条真实笔记来说“下午三点和组里确认预算之后去银行办卡。”如果按关键词规则“银行”很容易让人联想到生活可整条笔记的核心其实是“确认预算”这显然属于工作。硬写规则的话你要列多少个例外才能覆盖这种情况与其这样不如让模型自己从标注好的笔记里学习规律把“预算”“确认”“组里”这些组合词作为工作类的证据。所以我决定做一个基于机器学习的自动打标签工具让模型根据整篇内容的语义信息判断最合适的标签。这个项目本质上是一个文本多分类任务输入是一段本地笔记文本输出是一个预定义的标签。难度不大但好处非常直接以后我只需要把笔记丢进一个文件夹分类和归档都可以自动完成。1.2 模型选型朴素贝叶斯和逻辑回归的选择对于这种场景第一反应就是拿朴素贝叶斯来试。它简单、训练极快在小样本文本分类上表现也说得过去。但实际跑了交叉验证之后我放弃了它。原因倒不是精度不够而是朴素贝叶斯默认特征之间相互独立中文文本里词和词之间的搭配关系很常见“机器学习”和“机器”“学习”同时出现时模型会重复计算信息导致一些类的概率被过分放大。虽然最终分类结果未必差但后续我想用预测概率来做归档阈值它的概率输出不太靠谱经常出现 0.99 这种过于自信的结果小样本下尤其明显。换成逻辑回归之后效果明显更好。可以把逻辑回归理解成一个给词语打分再求和的分类器每个词对每个类别都有正或负的权重一篇笔记属于哪个类别就看所有词的加权得分加总后哪个类最高。它不像朴素贝叶斯那样要求特征独立权重之间可以自动互相抵消和补充所以面对“确认预算”这种组合词它会学习到“预算”对工作类有正向贡献而不是独立地把“预算”和“银行”各算一遍。在 500 条左右小样本上它的训练时间只有一两秒预测单条笔记的时间不到一毫秒完全够用。我做了一张选型对比表方便阅读定位维度朴素贝叶斯逻辑回归训练速度极快快预测速度极快快概率校准一般容易极端较好可调阈值小样本表现尚可更好可解释性可看特征概率可看权重排序内存占用低中最终选了逻辑回归还有一个原因是可以配合class_weightbalanced来处理类别不平衡。后面在样本不够均衡时这个参数帮了大忙。至于为什么不直接上深度学习或预训练语言模型原因很简单数据量只有几百条用复杂模型必然过拟合而且本地部署一个大规模模型加重了环境依赖对一个“自己用”的小工具来说性价比太低。1.3 文本向量化为什么选择 jieba TF-IDF中文和英文不一样英文单词天然有空格分隔中文必须先分词。我用了 jieba因为它在日常项目里足够快词典也全对口语化的笔记文本识别得不错。分词之后文本从“今天 开会 讨论 项目 进度”这样的形式才能进入后续流程。如果你处理的是英文笔记完全可以换成nltk或者spaCy但本地中文场景下jieba 够用且省事。向量化选择 TF-IDF。CountVectorizer 只统计词频“的”“了”“我”这种高频词会成为噪声TF-IDF 会对这些常见词降权对每个文档中更独特的词加权。用生活里的类比来说TF-IDF 像一个“稀有度加权器”在公司里听见“预算”这个词比听见“我们”更容易让你判断这是工作内容。比如“预算”在一个只谈工作的笔记里出现两次它的 IDF 权重就会很高模型更容易捕捉这个信号。Word2Vec 之类的嵌入表示我也想过但需要额外的预训练模型还增加了本地部署体积对一个小项目来说有点杀鸡用牛刀。2. 数据准备与标注策略2.1 收集原始笔记与初步整理训练数据来源是我自己的笔记文件夹。为了方便处理我先把这个文件夹里所有 Markdown 文件统一转成 CSV每行包含文件名、正文文本和标签。我用了pandas的read_csv读入采集时只保留每个文件的前 1000 字因为一条笔记通常不会太长太长的正文会让模型把注意力放到后面的无效内容上。文件读取要小心编码我在 Mac 上生成的文本是 UTF-8从 Windows 同步过来的部分文件带着 BOM需要统一用utf-8-sig读取否则第一行会出现一个不可见字符特别坑。初步整理时还发现一个高频问题Markdown 文件里混着大量代码块、超链接和图片引用这些内容对分类没有帮助甚至会把模型带偏。我用一个正则表达式把![](...)、[文字](链接)和代码块中间的内容全部替换成空格。这一步不是可做可不做直接影响后面的向量化质量。如果你不清理模型可能会学到“http”“png”这类符号与某个标签的伪关联一旦新笔记里没有这些符号预测就会不稳定。给一个清洗函数的核心片段import re def clean_text(raw): # 去掉图片和链接 text re.sub(r!\[.*?\]\(.*?\), , raw) text re.sub(r\[.*?\]\(.*?\), , text) # 去掉代码块 text re.sub(r.*?, , text, flagsre.S) # 去掉多余的换行和空白 text re.sub(r\s, , text) return text.strip()2.2 打标签的小技巧数据量不大我一开始觉得手动给几百条笔记打标签不是难事真做起来才发现很容易前后标准不一致。比如一篇关于“读《深度学习》并做了摘抄”的笔记既像“读书”又像“工作”。所以第一步不是直接标注而是先定义标签体系。我最终只保留四个互斥标签工作、生活、灵感、备忘。自己有明确的分类边界训练出来才不容易混淆。标签不能太多我当时尝试过八个标签结果“思考”和“灵感”的边界越来越模糊连我自己都说不清更别说让模型学了。具体标注时我用了两个技巧一是从同一个文件夹里批量选中因为当初写某些笔记时已经放在了对应目录下目录名本身就是弱标签二是把剩下没有目录线索的笔记随机打乱一次性看 50 条快速判断绝大多数内容属于哪一类。每类我给到 80~120 条一共 400 条左右对逻辑回归来说已经是一个能出效果的规模。如果你连标签体系都不想定也可以用无监督聚类先跑一遍看模型自己聚出的簇是否大致对应你的几类意图。但日常项目要的是稳定可控个人建议还是先有标签再训练聚类只适合探索和早期数据洞察。2.3 数据清洗与文本预处理分词之前还需要对中文文本做一点清洗。数字和英文一般保留比如“GPT-4”“Python”“2024 年”这些信息有识别度标点符号可以去掉但句号、问号这类对情感判断有用对主题分类帮助不大我在主题分类任务里是直接删除的。停用词表我试过效果反而变差因为笔记文本里经常出现“我们需要考虑一下”这种口语化表达“我们”“一下”去掉之后就剩下“需要”“考虑”语义就偏了。后来我只是去掉了“的、了、吗、呢”这类纯语气助词。另一个重要的点是不要在清洗时过度压缩。有人喜欢把所有字连在一起或者只保留名词我试过之后发现会丢失动词信息。“推进”“汇报”“安排”都是典型的工作类动词保留它们对分类很关键。分词之后用空格连接得到一个干净的文本列这才交给 TF-IDF。如果你有固定的专业术语比如“微服务架构”最好在 jieba 的词典里把这个词加进去否则会被切成“微服务”和“架构”虽然不致命但会让特征表达变弱。2.4 标签一致性的简单校验标注完 400 条之后我并没有急着训练而是先跑了一个简单校验把标注结果按标签分组随机抽取每组 10 条自己重新看一遍看有没有明显标错的。这一步很多人会跳过但我觉得很有必要。因为我当时就把一条“周末去超市采购清单”标成了“工作”原因是里面提到了“对接供应商”但整篇内容明显是生活采购。重新校验之后我修正了大概 20 条类似问题占总样本 5%。如果你不做这个动作模型会努力学着把“供应商”和“工作”绑定等真正预测到一条生活采购笔记时就会出错。如果时间允许还可以再找一个人帮你复标哪怕只是抽 50 条计算一下标注一致性。一致性低于 0.7 就说明标签定义不够清楚需要回去修改。不过个人项目里通常只有自己一个人用我这个步骤就用“隔一天再看一遍”来代替了。间隔一天能避免短期记忆带来的惯性判断更容易发现前后标准不一致。3. 模型训练与调参3.1 构建训练管道我用 scikit-learn 的Pipeline把向量化和分类器串起来这样在交叉验证时不会出现数据泄漏。具体做法是先对原始文本分词再定义TfidfVectorizer最后是LogisticRegression。这样做的好处是预测时只需要把整条管道joblib.dump保存下来不用单独保存分词结果。核心代码是这样的import jieba import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.model_selection import cross_val_score def tokenize(text): return .join(jieba.lcut(text)) df pd.read_csv(notes_labeled.csv) df[text_clean] df[text].apply(clean_text) df[text_cut] df[text_clean].apply(tokenize) pipeline make_pipeline( TfidfVectorizer(ngram_range(1, 2), max_features5000, min_df2, sublinear_tfTrue), LogisticRegression(max_iter1000, class_weightbalanced) ) scores cross_val_score( pipeline, df[text_cut], df[label], cv5, scoringf1_macro ) print(CV F1 macro:, scores.mean())这段代码里有两个细节值得展开说。sublinear_tfTrue把词频做了对数变换避免一篇特别长的笔记因为一个词出现 10 次而获得过于离谱的权重。class_weightbalanced表示按类别频率自动调整权重后面在“灵感”类只有 70 条、“工作”类有 130 条时这个参数能防止模型把所有内容都往工作类推。如果你只用准确率评估这个操作可能看不出差别但用 macro-F1 时就能明显感受到变化。3.2 关键参数是怎么确定的ngram_range(1, 2)是我对比之后定下来的。只做单个词时“机器学习”被切分成“机器”和“学习”模型分不清这两个词是一起出现的还是各自出现的加入二元词组后“机器学习”成为一个特征能把类似“机器 学习 算法”这种语境识别得更稳。再用三元甚至四元词组意义不大因为笔记文本里很少有稳定的三词搭配特征矩阵会变得非常稀疏。max_features5000是结合语料规模来定的。我总共只有 400 条笔记每条平均几十到一百字切出来的有效词总数不到一万。取前 5000 个词既保留了绝大多数有效信号也压缩了特征空间。min_df2表示一个词至少要在两篇笔记里出现否则很可能是在某篇文章里打错字或者是不规范的自造缩写留着只会增加噪声。我一开始没有设min_df结果出现了一些只出现过一次的人名、地名模型记下来后导致单个样本对结果的影响过大。还有max_df我没有使用固定值因为去掉出现次数过高的词往往会把“工作”里的“项目”也去掉而这种词其实是有类别区分度的。你可以用max_df0.95做个测试但我的结论是如果语料本身就比较干净TF-IDF 的固有降权已经能处理好常见词没必要再做一刀切。调完参数后我还在同一个管道上试过C值正则化强度。默认C1.0在小样本上表现稳定调到 0.5 反而过平滑调到 2.0 则容易过拟合。这个项目里我没有做贝叶斯搜索靠几次手动交叉验证就确定了最优值因为数据规模太小网格搜索的结果波动也很大。3.3 交叉验证与评估指标我用了 5 折交叉验证因为 400 条数据分成 5 份每份 80 条训练集 320 条测试集 80 条这样的划分比较稳定。如果只用一次 train_test_split运气不好会抽到某个标签特别集中的测试集评估指标虚高或者偏低参考价值不大。交叉验证的过程也会让我对模型的泛化能力有个更冷静的判断。评估指标我选了f1_macro也就是每个类别算 F1 再取算术平均。四个标签数量并不一致如果选accuracy模型只要把更多笔记判为“工作”准确率也会很好看但这个结果对整理笔记没用选 micro-F1 同样会被大类数量带偏。Macro-F1 对每个类别一视同仁少数类做得不好分数立刻就会掉下来这跟我的目标是一致的。实际跑下来四类 macro-F1 在 0.82 左右对一个小样本文本分类器来说已经很够用了。如果你发现自己类的数量差距特别大还可以打印出每一类的 precision 和 recall看具体是哪一类拉低了分数而不是只看一个总分。3.4 小心验证集和测试集混用我在第一次调参时犯过一个新手错误用同一份交叉验证结果反复调参不停打开 CSV 看哪些样本被分错然后根据这 400 条样本的特点去加规则、去停用词最后发现验证集分数越来越高但真正等新笔记进来后效果非常差。这就是典型的信息泄漏不是数据上泄漏而是人为地把验证集的信息引入了调参过程。正确的做法是把数据分成训练集和留出集调参阶段只碰训练集和交叉验证留出集最后评估一次。个人项目里因为数据少留出 80 条会心疼但这个损失是必要的。后面我把数据重新切分用 320 条训练80 条留出调参过程只在 320 条上做 5 折交叉验证等所有参数都定了再在 80 条上跑一次。这个流程走下来模型在留出集上的 macro-F1 是 0.80与交叉验证分数基本一致我心里就有底了。4. 模型应用与落盘4.1 训练完成后保存模型交叉验证通过后我在全量数据上重新训练了一次然后直接用joblib.dump保存。这里有个经验不要再保存一个训练时用的 DataFrame 对象只需要保存管道本身。管道里已经内含 TF-IDF 词汇表和逻辑回归权重下次加载后可以直接对原始文本做预测。文件体积也不大我的模型只有 200KB 左右放哪个目录都不占地方。保存代码import joblib pipeline.fit(df[text_cut], df[label]) joblib.dump(pipeline, models/notes_model.joblib)注意保存路径不要带中文有些部署环境对中文路径的处理很诡异。我会在项目根目录建一个models/目录模型统一放进去分词工具 jieba 虽然有自己的缓存但模型文件本身不依赖 jieba 的缓存所以拷贝到另一台机器上也能正常预测只要那台机器安装了相同版本的 jieba 和 scikit-learn。如果你换版本后加载模型遇到报错大概率是 pickle 版本兼容问题重新在目标机器上用相同版本训练一次是最省事的解法。4.2 做一个命令行快速打标签工具模型落盘后最直接的应用是命令行工具。我写了一个几十行的 Python 脚本读取参数里的文本输出预测标签和各类别概率。概率输出很重要因为它能让我判断模型是不是在瞎猜。如果最高概率只有 0.4我会把这条笔记放进“待人工整理”目录而不是直接归档。这个思路类似于给模型加一个置信度门槛宁可漏判也不误判。核心部分import sys import joblib import jieba pipeline joblib.load(models/notes_model.joblib) def predict_one(text): text_clean clean_text(text) text_cut .join(jieba.lcut(text_clean)) probs pipeline.predict_proba([text_cut])[0] label pipeline.classes_[probs.argmax()] return label, probs.max() if __name__ __main__: content sys.argv[1] label, confidence predict_one(content) print(f预测标签: {label}, 置信度: {confidence:.2f})实际用下来命令行版本适合把文本复制粘贴进去临时用。它还有个好处在自动化工具出问题时保留一条兜底的人工调用通道。命令行工具不要输出太花哨只返回标签和置信度就够了后面要接别的程序也方便。4.3 改造为文件夹监控自动归档光有命令行还不够我又加了文件监听功能。用的是 watchdog 这个 Python 库监听指定目录每当有新的.md文件出现或已有文件被修改就读取内容做预测然后移动到对应标签文件夹。这一步最容易被忽视的是文件写入时机编辑器保存文件不是一次原子操作可能会触发多次事件如果不做延迟处理程序可能在文件只写了一半时就去读内容最后得到一个残缺的预测文本。我的做法是监听器收到事件后等 3 秒再检查文件大小是否在持续变化如果不再变化才执行预测。如果预测置信度低于阈值文件不移动而是复制一份到08_待整理目录并在原文件旁边留下一个简单的标记文件方便我回查。阈值我设置在 0.5太低会误归档太高会增加人工整理负担0.5 对四分类场景比较均衡。这段监控脚本不算长核心就是FileSystemEventHandler里重写on_modified和on_created。我不建议在这个流程里加太复杂的重试逻辑跑了两周之后发现预测失败的大多是空文件、二进制文件或纯图片的 Markdown遇到这类情况只要跳过就行。from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler import time import os class NoteHandler(FileSystemEventHandler): def on_created(self, event): self._handle(event) def on_modified(self, event): self._handle(event) def _handle(self, event): if not event.is_directory and event.src_path.endswith(.md): time.sleep(3) file_size os.path.getsize(event.src_path) time.sleep(1) if os.path.getsize(event.src_path) ! file_size: return # 文件仍在写入 label, conf predict_one(open(event.src_path, encodingutf-8-sig).read()) if conf 0.5: # 移动到对应标签目录 ...4.4 实际运行一周的效果记录脚本上线后我并没有全自动跑前一周保持“半自动”模式每次自动归档后都把结果写进一个日志文件晚上抽几分钟看一下有没有明显误判。一周下来一共归档了 43 条笔记正确判断 36 条误判 7 条其中 4 条是置信度低于 0.5 但被强行归档的另外 3 条属于标签边界本身模糊我自己也拿不准该归哪一类。这次实测给了一个很重要的修正方向阈值需要分成类来设置不能统一用 0.5。工作类样本多0.5 足够可靠灵感类样本少预测概率普遍偏低如果也按 0.5 卡会漏掉很多真正想保留的灵感笔记。最终我把阈值改成每类单独设置工作类 0.5灵感类 0.4生活类 0.5备忘类 0.6。这种配置没法通过一次交叉验证得到只能在真实使用中慢慢调。5. 常见问题与排查技巧实录5.1 中文乱码与编码问题日常项目最常见的坎就是编码。我在 Mac 上读取到的 Markdown 文件默认是 UTF-8但有一部分从 Windows 同步过来的文件带 BOM用encodingutf-8读进来后第一行开头会多出\ufeff。这个字符模型不认识也不影响分类但会在清洗后的文本里变成一个孤立“词”严重时会挤掉一个真正有意义的词的位置。后来我统一用utf-8-sig读取BOM 自动被剥离。还有 GBK 编码的旧文件这类内容如果量很少我直接跳过不做转码因为偶尔一个乱码样本对整体分类器没有帮助反而增加判断成本。另外提醒一下在 Linux 服务器上运行脚本时如果文件是由某人上传的 Windows 文本换行符是\r\n正则里\r可能混进分词结果。清洗时用text.replace(\r, )提前处理会比较省心。文件系统编码问题通常不会影响模型精度但会扰乱你的数据读取过程浪费大量时间。5.2 预测速度慢怎么办单条笔记的预测时间主要由分词决定TF-IDF 向量化和逻辑回归几乎不耗时。jieba 首次加载词典需要一两秒之后单条文本分词通常在几毫秒到几十毫秒。如果你需要批量预测几千条笔记速度才值得优化。我开始时直接把整篇 Markdown 全部丢进去分词速度还行但向量化之后产生了大量噪音词。后来改成只取标题和正文前 500 字按字符计算这样既保留了主题又减少了无效内容。如果你有大量短文本要预测另一个思路是并行分词在多核机器上用concurrent.futures.ProcessPoolExecutor把数据分成几份速度基本可以线性增长。但我这个场景没到那个量级单条预测一天最多几百次完全没有瓶颈。5.3 样本不均衡与预测漂移我的四类标签里工作类最多灵感类最少一开始模型几乎把所有模棱两可的笔记都判成工作。打开预测结果看发现“灵感”类的召回率只有 0.5这就是典型的类别不平衡。我加了class_weightbalanced问题解决了大半。如果你不想用这个参数也可以试试随机欠采样把小类复制几份但这种方法在小样本上容易过拟合我还是推荐参数方案。还有一个更隐蔽的问题预测漂移。随着时间推移笔记里会出现新词汇比如之前完全没有的“AIGC”“数字人”这类词TF-IDF 词汇表里没有它们向量化时会被直接忽略模型只能靠周边的旧词猜置信度会下降。所以模型不是训好就一劳永逸我给自己定了个节奏每两周或每新增 200 条笔记后把新数据合并进来增量重训一次这样标签体系才会跟上语言习惯的变化。重训时建议保留旧的训练数据而不是直接用新数据否则已经学会的旧词特征会慢慢遗忘。6. 这个项目的后续演进方向如果你把自己当成一个长期使用者这个笔记归档器还有很多可以继续打磨的地方。我现在最想做的是把预测结果回写成笔记文件里已有的 YAML front matter让原本只支持文件名分类的笔记软件也能直接读取标签。很多笔记软件支持tags:字段在保存文件时自动把这个字段填充进去检索体验会好很多。另外可以给每个标签维护一份“热点词表”从逻辑回归权重中取出前 20 个正权重词定期生成一份“我最近在关注什么”的词云。这种方式不算复杂但每天看自己的笔记被模型提炼成几个关键词会有一种意外的成就感。还可以把预测结果接入一个简单的日历提醒当“备忘”类笔记里出现日期时自动创建一个提醒事件这已经超出文本分类的范畴但有了可靠标签之后做后续加工就顺手很多。我个人的体会是这个项目并不需要更高深的模型它的价值在于把一个数据管道的闭环打通从原始文本、到标注、到训练、到部署、再到反馈重训。很多人卡在模型精度上反复调参其实对一个日常工具来说稳定能跑、结果可用、自己愿意复用比任何指标都重要。最后再分享一个小技巧如果你也打算做类似工具阈值一定要从低到高慢慢调先保证不误归再追求召回实际使用体验会好很多。如果你也想拿机器学习做点真正有实际产出的东西不妨从自己的笔记开始动手成本低见效快。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询