news 2026/10/17 10:59:30

NLP特征提取与无监督学习:从词袋到BERT的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NLP特征提取与无监督学习:从词袋到BERT的实战指南

做过文本类项目的人,大概率都有过这样一个困惑:为什么BERT都出来这么久了,学校里还在讲TF-IDF、词袋模型这些“老古董”?也有不少朋友从各种教程里学会了调用预训练模型,但真到做业务的时候,又不知道手里那几百个项目的向量到底该怎么用。

这篇内容想聊聊我这些年做NLP项目时对特征提取和无监督学习的理解。从最传统的词袋模型,到Word2Vec,再到BERT的动态表征,以及围绕这些特征展开的聚类、主题建模、降维可视化。标题虽然写着“第6-7节”,但本质上这就是一条从“手写特征”走向“预训练特征”,再从“特征”走向“业务洞察”的完整链路。内容不深,但足够实在,适合刚入门自然语言处理、或者已经在用预训练模型但想搞清楚“为什么这么用”的朋友。

先说几个经常被问到的核心问题:词袋模型和TF-IDF特征在BERT时代到底还有没有用?Word2Vec和BERT生成的向量,本质区别在哪?拿到几千上万条文本,怎么用无监督的办法快速看一遍数据分布?BERT向量落到具体业务里,常用的有哪几条路?

我尽量用大白话把这些讲清楚,顺带给出可直接跑的代码和参数选择的思路。

1. 特征提取这条线,为什么从词袋一直走到BERT

1.1 词袋与TF-IDF:能用来做什么,又卡在哪

最早期的文本特征提取,本质上就是“数词”。一段话进来了,先分词,再统计每个词出现的次数,于是每个句子就变成了一个维度等于词典大小的稀疏向量。这个办法叫词袋模型(Bag of Words)。

词袋模型的问题很明显:它只看词频,不看词序。“我打你”和“你打我”在词袋眼里是同一个向量。这不算致命,因为它起码把文本变成了机器能算的数字。更麻烦的是高频停用词(“的”“了”“在”)会占据很高的权重,真正有区分度的词反而被淹没。

于是就有了TF-IDF。它的核心就一句话:如果一个词在当前文本里出现得多,但在整个语料里出现得少,那它就很重要。这相当于给每个词打了两个分,一个是词频分,一个是“稀有度”分。稀有度怎么算呢,就是总文档数除以包含该词的文档数,再取对数。这个设计逻辑非常朴素,但直到今天它仍然是文本匹配、关键词抽取任务里的强大基线。

我自己现在还用TF-IDF的场景,是给数据做冷启动标签。比如刚拿到一批“某电商平台用户评价”,又没标注又没词表,我就先把TF-IDF跑一遍,抽出每个类簇里的高权重词,快速判断数据主题。这事BERT也能干,但在十几万条零标注文本上,TF-IDF几秒钟就出来了,BERT光过一遍得跑几分钟,而且未必比TF-IDF的抽取结果更适合让人“肉眼读”。

1.2 稠密词向量:把“意思”塞进几百个数里

词袋和TF-IDF生成的向量维度动辄上万,而且还很稀疏,大多数位置是零。这种向量的问题不仅在于计算慢,更在于它表达不了语义关系。“苹果”和“香蕉”明明是两类水果,但在TF-IDF向量空间里,它们可能相隔十万八千里,因为它们的共现词几乎不重叠。

Word2Vec(还包括GloVe、FastText)解决的正是这个问题。它用了一个非常朴素的思想:一个词的意思,由它上下文的词决定。训练的时候,模型让出现在相似语境里的词,在向量空间里彼此靠近。于是几百维的稠密向量,就把“语义距离”压缩进了几何距离。

生活化理解:Word2Vec像是给每个词做了一张“身份证照片”,把它的长相固化下来。优点是快、轻、离线可用、适合中小规模数据;缺点是无论“苹果”出现在“吃苹果”还是“苹果公司发布新机”,它的向量都是一样的。这种“一词一义”的静态设定,在遇到多义词时就会吃大亏。

1.3 BERT嵌入:语境感知的“动态词典”

BERT在特征提取上的革命性,在于它给每个词生成的向量不是固定的,而是随着所在句子变化的。同一句子里“苹果”前面的词是“切开”,向量主干指向水果义;如果上下文是“发布会”,向量主干就偏向公司义。这个能力不是魔法,而是预训练阶段用海量语料和Transformer的双向注意力机制学出来的。

用句人话概括:词袋模型只看“有没有这个词”,TF-IDF看“这个词多有代表性”,Word2Vec看“这个词大概属于哪一类意思”,BERT看“这个词在当前这句话里到底是什么意思”。因此BERT能做到很多传统方法做不到的事,比如同义词替换检测、语义相似度匹配、跨语种对齐,以及长文本的语义编码。

不过也要说句公道话。BERT向量虽然语义信息丰富,但它不是万能的。它的嵌入向量在不同层级上“编码”的信息是不一样的:底层更偏向词法和句法特征,中层偏语义,高层偏上下文和任务相关特征。这就引出实操里一个很重要的问题:到底把哪一层的输出当特征用?这个问题后面专门讲。

2. 无监督学习在文本里的三个主要打法

2.1 文本聚类:K-Means和它的脆弱之处

拿到一堆文本,最想做的第一件事就是看看这堆东西能不能分成几类。无监督聚类恰好承担这个角色。最常用的算法还是K-Means,虽然它简单古老,但速度极快、结果好解释。

K-Means做的事情非常简单:先随机指定K个中心点,然后把每一条文本分给离它最近的中心,再根据每个簇内文本的平均位置更新中心,不断重复直到中心点不再变化。这个过程本质上是“让簇内尽可能紧凑、簇间尽可能分离”。

但如果直接把TF-IDF的高维稀疏向量喂给K-Means,效果往往很差。为什么?因为在高维空间里,向量之间的余弦距离趋同,欧氏距离被大的模长主导,稀疏零点也干扰簇心更新。所以实际操作中,一般会先用SVD或者PCA降到低维,再用K-Means。这就引出一个经验法则:高维稀疏特征+聚类,先降维再聚类;低维稠密特征(如BERT向量),直接聚类问题不大,但建议先标准化。

另一个常见的坑是K值怎么选。K-Means要求提前指定簇数,但你拿到新语料往往是不知道有几类的。所以一般会用“肘部法则”画一条偏差平方和(SSE)随K变化的曲线,找一个“拐点”。不过说实话,这个拐点在真实数据上经常不明显。我的做法是用轮廓系数,跑K从2到20,找出轮廓系数最大的K,再结合业务可解释性做人工判断。两个指标结合着看,比只看一张图靠谱得多。

2.2 LDA主题模型:把文档集合“摊开”看

聚类是把“文本”分到不同桶里,主题模型则是在问:这批文档里反复出现的“话题”是哪几个。LDA(隐含狄利克雷分配)是这里最经典的方法。

理解LDA最生活化的方式,是把每篇文档想象成一个“混合饮料”,主题是里面的“基料”。同样的文档集合里,可能有20%的内容在谈性能测试,30%在谈界面交互,50%在谈部署运维。LDA做的事情,就是根据词共现的规律,把这些潜在主题反推出来,并且给出每个主题下的关键词分布。

LDA有个特别好的用处:当你不知道数据里到底有哪些内容区块时,它能在不做任何标注的情况下给你一张“内容地图”。比如我处理过一批某社区论坛的帖子,用LDA跑出10个主题,很快发现其中几个主题对应的是广告帖,于是直接在预处理阶段当噪声过滤掉。这种用途,聚类也能做,但LDA给出的“关键词描述”更直观,和业务同事沟通时尤其好用。

LDA的两个关键参数是主题数K和超参数α/β。K的选择跟聚类里的K一样让人头大,常用办法是用困惑度(perplexity)或者主题一致性(coherence score)来调。经验上,主题一致性比困惑度更贴近人的判断。α建议设小一点(比如0.1),让每篇文档尽量聚焦在少数几个主题上;β同理设小一点,让每个主题尽量集中到少数词上。这个从贝叶斯先验角度也好理解:小α意味着“文档主题分布稀疏”,小β意味着“主题词分布稀疏”。

2.3 降维可视化:t-SNE让高维向量“显形”

聚类和主题模型都在做“数字层面的分组”,但人类看不了几百维的向量。于是降维可视化就成了无监督学习里非常实用的附赠武器。

PCA是线性降维,速度最快,但它只能捕捉全局的线性结构。BERT向量里丰富的非线性语义结构,PCA往往压制到二维时都糊成一团。这时t-SNE效果要好得多,它的设计目标是让高维空间中距离近的点在二维平面上仍然靠近,距离远的点尽量分开。代价是计算量很大,几万条样本还算能忍,几十万条就非常吃内存了。

实操中我通常在聚完类之后再做一次t-SNE,把BERT句子向量降到二维,用簇标签给点上色,一眼就能看出聚类结果到底是“边界清晰”还是“混沌一片”。这个图用来给非技术的业务方看,比单纯讲轮廓系数直观得多。

需要注意一个细节:t-SNE的超参数perplexity非常敏感。它大致表示“每个点考虑多少个邻居”,默认30,但如果样本量小,建议把perplexity降到5到15之间,否则图上的点会互相挤成一团。多跑几个perplexity值,挑一个视觉上分离度最合适的。

3. BERT特征提取实操:把预训练模型变成向量生成器

3.1 环境准备与模型选择

做BERT特征提取,我的主力工具是transformers库加PyTorch。如果只跑推理不做训练,用CPU其实也勉强能行,但批处理大量文本还是建议上GPU,哪怕是一块入门级显卡也行。

模型选哪个?通用领域中文任务,推荐用中文预训练模型。英文同理,选对应语言的模型。任务偏短文本相似度,可以把输入截断到128以内;任务偏长文本,考虑序列长度512,或者分段提取再做聚合。这一节代码以经典中文模型为例。

3.2 三种提取方式:CLS、mean pooling、按token取层

拿到BERT的输出,最常见的特征提取方式有三种,这里逐一说明差异:

第一种是取[CLS]位置的向量。预训练模型训练的时候,[CLS]位置的输出被设计成“整句表征”。但实际使用中,它的效果不一定最好,因为[CLS]在被训练时更多是为“句子对关系判断”服务的,而不是为通用特征服务的。所以直接拿CLS当句向量,简单,但可能损失细节。

第二种是mean pooling,也就是把所有token的输出向量按位取平均。这个做法忽略不同token的权重差异,但由于它整合了全句所有词的信息,在句子相似度任务上往往比CLS更稳。多数项目我默认先用mean pooling。

第三种是按需取层。BERT有12层(base版),每层输出都可以当特征。底层偏词法,中层偏语义,顶层偏任务。经验上,倒数第二层在通用语义任务上往往表现最好。如果你想跟倾向“更泛化”的特征,可以试试把最后四层拼接或加权求和。这个方法计算量大一些,但效果上限更高。

我的默认配方:句向量取倒数第二层的mean pooling,维度768,然后直接进入聚类或检索流程。如果要复用多次,可以先离线把所有文本的向量存成npy文件,后面随时加载,不重复算。

3.3 参数与细节:max length、batch size、layer选择

参数选择里最有讲究的,反而不是模型结构,而是几个工程细节。

第一个是truncation策略。输入序列一旦超过max length,直接截断会丢掉尾部信息。若是长文本分类,我一般先做滑窗切段,每个片段单独编码,再取平均或最大池化当全文向量。

第二个是batch size。虽然推理比训练省显存,但batch size太大会直接OOM。经验值是12G显存跑base模型,建议batch size在16到32之间。遇到显存不够,不是因为模型太大,而是你没开torch.no_grad()和model.eval(),导致梯度计算占了一大半显存。推理阶段务必把这两步落实,显存占用能降一半还多。

第三个是向量是否标准化。BERT向量各维度方差不同,直接做欧氏距离计算时,某些维度会主导结果。我通常在提取之后做L2归一化,这样后续无论算余弦相似度还是跑K-Means,数值都更稳定。实践中这一步对聚类效果的提升非常明显。

4. 把无监督学习和BERT结合起来:一个可复现的聚类流程

4.1 整体流程设计

特征工程和无监督学习单独拆开看都简单,但一个能落地的完整流程,最好按下面这个顺序来:先清洗文本,再做文本编码,得到BERT向量;然后降维,用PCA降到约100维;接着跑K-Means聚类;最后用t-SNE可视化,并回看每个簇的高频词。

为什么降维后再聚类这一步不要省?因为BERT原始向量是768维,在这么高的维度下跑K-Means,距离值会被拉平,簇心更新也变得不稳定。降到100维左右,既保留了绝大多数语义信息,又让聚类算法更稳当。这一步属于典型的技术细节,但省了它,后面就是玄学调参。

4.2 完整代码示例

下面给一套可直接跑的流程代码,用英文短文本举例,中文只需换成中文模型和分词方式即可。

import torch import numpy as np from transformers import AutoTokenizer, AutoModel from sklearn.cluster import KMeans from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler model_name = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModel.from_pretrained(model_name) model.eval() texts = [ "the battery life is amazing", "great camera quality for the price", "the app keeps crashing on startup", "interface is clean and easy to navigate", "delivery was faster than expected", "customer support never replied", ] def encode_texts(text_list, max_len=64): encodings = tokenizer( text_list, max_length=max_len, padding=True, truncation=True, return_tensors="pt", ) with torch.no_grad(): outputs = model(**encodings) # 取倒数第二层的输出:[batch, seq_len, hidden_size] hidden = outputs.hidden_states[-2] # 用 attention mask 做 mean pooling,忽略 padding 位置 mask = encodings["attention_mask"].unsqueeze(-1).float() masked = hidden * mask pooled = masked.sum(dim=1) / (mask.sum(dim=1) + 1e-8) return pooled.numpy() # 需要设置 output_hidden_states=True # 因此加载模型时改为: model = AutoModel.from_pretrained(model_name, output_hidden_states=True)

上面这25行代码囊括了“加载模型、编码、取倒数第二层、masked mean pooling”这四个关键动作。其中masked mean pooling是整段代码最容易写错的地方。很多初学者直接对hidden_states取mean,没考虑padding位置,结果就是每条样本的“有效长度”不同,短文本被大量pad位“稀释”,向量整体向零向量方向偏移。加了attention_mask加权之后,这个问题就消失了。这个细节我在不同项目里至少帮别人排查过三次。

拿到向量后,下一步是降维和聚类:

vec = encode_texts(texts) vec = StandardScaler().fit_transform(vec) pca = PCA(n_components=4) vec_pca = pca.fit_transform(vec) kmeans = KMeans(n_clusters=2, random_state=42, n_init=10) labels = kmeans.fit_predict(vec_pca) print(labels)

关于StandardScaler标准化,单独说一句。BERT向量的维度分布不是标准的,某些维度方差大,某些方差小,如果不标准化,聚类会被少数高方差维度主导。加了标准化这一步,后续特征都保持在类似尺度上,聚类结果更稳定。n_init设成10,是让K-Means多试几组初始中心,取最优结果,避免局部最优。

4.3 结果评价与调参

聚类做完,结果怎么评价?无监督没有标准答案,只能从多个侧面交叉验证。我常用三个视角:

第一是人工抽样看簇内文本。每个簇抽出三到五条文本,看它们是否在讲同一件事。这是最朴素也最可靠的方法。

第二是看轮廓系数。取值范围从-1到1,越接近1表示簇内紧密、簇间分离。但要注意,真实长尾数据里轮廓系数0.3都算不错,别拿教科书里的0.7来对标。

第三是算簇内高频词。把每个簇的文本拼起来,跑TF-IDF取Top词,看这些词是否符合语义直觉。这一点很好用,尤其当聚类结果不好时,高频词列表会显示出簇内混入了明显无关的主题。

调参路径上,我会依次调:K值(聚类)、perplexity(可视化)、池化方式(CLS还是mean pooling)、层数选择(倒数第一还是第二层)。每次只动一个变量,记录指标变化,别同时改两个以上,否则你根本不知道是哪个改动起了作用。

5. 常见问题与排查技巧实录

5.1 出问题最多的地方往往不在模型

先放一张问题速查表,都是实际项目里高频踩过的坑:

现象可能原因解决方案
向量全是极其接近的值忘了关梯度推理时加torch.no_grad()和model.eval()
短文本和长文本聚类混在一起masked mean pooling写错用attention_mask加权取平均
聚类结果每个簇都差不多没标准化或没降维先StandardScaler,再PCA降维
t-SNE图糊成一团perplexity和迭代次数不合适perplexity设15~40,多跑几次看
直接调用CLS向量效果不稳定CLS并非通用句向量改用倒数第二层mean pooling
几万条文本编码太慢没做批处理或没上GPU用batch推理,至少换一块入门卡

这里面最容易被忽略的还是推理模式没关。很多人以为BERT编码文本是“纯前向计算”,没注意默认的PyTorch模型其实是train模式,里面的Dropout还在随机遮蔽节点。这样出来的向量每次都在变,同一句话编码两次结果差不少。做特征提取时先调model.eval(),是比选模型更优先的事。

另一个高频问题是“输出维度和预期不符”。有人拿长文本编码后,发现序列长度被截断,向量信息严重丢失。解决办法是滑窗切段,但如果只是做聚类,一个更省事的方案是先概括——模型编码摘要后的短文本,几十字足够保留主干语义。不过这个方法对正式项目不推荐,只适合快速看数据分布时凑合。

5.2 避坑心得:真正想清楚“特征给谁用”

最后说点原则性的事情。特征提取不是越“先进”越好,而是要看下游任务是什么。

如果下游任务是精确的语义匹配、问答检索,那么BERT向量完胜。如果下游任务只是给一万条无标注文本快速分类打标,传统TF-IDF加K-Means可能两分钟搞定,而BERT向量加聚类可能要半小时,结果未必更准。如果任务涉及小样本或者类别极不均衡,那先向量化再做聚类找困难样本,再用这些样本做主动学习,反而是一条非常高效的闭环。

我做过一个典型的案例:某新业务要建立一套客服对话的分类体系,可用标注为零。先用BERT向量加聚类分出12个大类,专家在每类里标注了50条,再训练一个轻量文本分类器,准确率达到可接受水平。整个过程下来,真正花时间的设计不在模型,而在“特征怎么提、簇怎么定义、人工怎么介入”。

所以总结起来就一句话:BERT给了我们一个很强的语义特征底座,但无监督学习这套“先看数据、再定方案”的方法论,才是让底座真正发挥价值的黏合剂。两者是配合关系,不是替代关系。

后面如果再往下走,无非是两条路:一条是把特征接到检索、匹配、RAG这类业务里,另一条是做微调让特征更贴近具体任务。不管哪条路,前面这“第6-7节”的地基打得牢不牢,往往决定了后面能走多远。踩过几次坑之后,我现在拿到任何一批新文本项目,反而越来越愿意先老老实实跑一遍特征提取和无监督聚类,因为这可能是整个项目里性价比最高的一步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 10:58:48

YOLOv11嵌入坐标注意力提升小目标检测mAP实践指南

前段时间给手头的检测任务做优化,想在不换模型的前提下尽可能提升mAP。试过调NMS参数、改anchor分配、调loss权重,效果都不明显。后来在YOLOv11的backbone末尾和neck部分各嵌入了一个Coordinate Attention(坐标注意力)模块&#x…

作者头像 李华
网站建设 2026/10/17 10:57:18

AI驱动SDLC技能提升:从需求分析到运维的全流程实践

1. 先给这个题目定个调:AI 到底在 SDLC 里解决了什么问题 我做了多年软件交付,最近两年把 AI 工具深度嵌进了自己的日常开发流程,最大的感受是:AI 驱动的 SDLC 技能提升,不是让你从“不会写代码”变成“会写代码”&…

作者头像 李华
网站建设 2026/10/17 10:54:57

前缀和的逆运算——差分数组:区间修改高效算法详解

先聊一个老生常谈的话题:很多人刷题刷到“差分”这个知识点时,第一反应是“又是一个看起来很数学的东西”,然后容易被名字劝退。但实际接触下来你会发现,差分的核心思想只用一个生活场景就能讲清楚——记录相邻两天的体重变化量&a…

作者头像 李华
网站建设 2026/10/17 10:53:17

Shenandoah GC实战:把JVM停顿压到几十毫秒的关键机制与调优

去年我在排查一个线上服务时,碰上一件特别典型的事:接口平均耗时很稳,P99 却每隔几十秒就突然跳到几百毫秒,随后又恢复。查到最后,问题出在 JVM 垃圾回收(GC),G1 在混合回收阶段需要…

作者头像 李华
网站建设 2026/10/17 10:53:04

C++植物大战僵尸模型:类设计、碰撞检测与对象池实战解析

简介:基于C实现的植物大战僵尸塔防游戏模型,面向对游戏开发感兴趣的C初学者与进阶者,可系统理解塔防游戏从逻辑设计到代码落地的全过程。资源内含可直接运行的exe程序、完整的C源码以及VS工程文件,方便边运行边对照代码逐模块推演…

作者头像 李华