排查一个线上问题,用户反馈某商品的自动标签系统突然开始乱打标——把“充电宝”分类成“食品”,把“羽绒服”标记为“电子产品”。查日志发现,新上线的NLP分类模块在处理某些特定商品描述时,相似度计算完全失控。根本原因?词向量模型加载时用了错误的维度配置,导致“充电”和“充电宝”在向量空间里距离比“充电宝”和“移动电源”还远。今天我们就从这个问题切入,聊聊NLP里最基础也最易踩坑的两个概念:词向量和文本分类。


词向量:文字的数字替身

早年做文本处理,最直接的方法就是one-hot编码。每个词对应一个超长向量,只有自己位置是1,其他全是0。这种方法简单粗暴,但问题很明显——维度爆炸且毫无语义信息。“手机”和“电话”的向量距离,跟“手机”和“冰箱”一样远,这显然不符合我们的认知。

后来Word2Vec这类模型出来,算是打开了新世界的大门。它的核心思想很直观:一个词的语义,可以由它周围经常出现的词来定义。看这段训练代码:

# 旧版one-hot做法,别这样写了
def one_hot_encode(word, vocab):
    vector = [0] * len(vocab)
    vector[vocab[word]] = 1  # 除了自己位置是1,其他都是0
    return vector
# 问题:50000个词就需要50000维,内存吃不消且没语义

# 改用词向量
from gensim.models import Word2Vec

# 训练自己的小模型
sentences = [["智能手机", "电池", "续航"],
             ["电池", "容量", "充电"],
             ["充电", "器", "快充"]]  # 领域相关语料很重要

model = Word2Vec(sentences, vector_size=100, window=3, min_count=1)
# vector_size通常设100-300,太小信息不够,太大容易过拟合
# window控制上下文范围,一般3-5效果比较好

vector = model.wv["充电"]
print(f"向量维度:{vector.shape}")  # (100,)
print(f"'充电'和'快充'相似度:{model.wv.similarity('充电', '快充'):.3f}")

这里有个实际调试的坑:预训练模型和自定义模型的维度必须对齐。我们线上问题就是加载了300维的预训练模型,但代码里写死了100维的查找逻辑,导致向量切片错位,语义完全乱套。

# 错误示例:混用不同维度的模型
pretrained_model = load_pretrained("tencent_embedding.bin")  # 300维
custom_layer = nn.Linear(100, 10)  # 期待100维输入
# 运行到这就崩了,维度对不上

# 正确做法:统一维度或做投影转换
if pretrained_model.vector_size != 100:
    projection = nn.Linear(pretrained_model.vector_size, 100)
    vector = projection(pretrained_vector)  # 300维转100维

现在更常用的其实是BERT这类上下文相关的词向量,同一个词在不同句子里有不同表示。比如“苹果”在“苹果手机”和“苹果好吃”里向量不同,这比Word2Vec的静态向量更聪明。不过对于入门来说,先理解静态词向量是关键基础。


文本分类:从词袋到深度学习

有了词向量,文本分类就好办了。最早期的词袋模型(Bag of Words)完全忽略词序,只统计词频。虽然效果有限,但在某些场景下依然有用:

from sklearn.feature_extraction.text import CountVectorizer

corpus = [
    "手机电池续航能力强",
    "电池充电速度快", 
    "屏幕显示效果出色"
]

vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)
print(vectorizer.get_feature_names_out())  
# ['充电', '出色', '屏幕', '电池', '续航', '能力', '速度', '手机', '显示', '效果']
# 注意:中文需要先分词,这里为演示简化了

词袋模型最大的问题是维度高且稀疏。后来TF-IDF做了改进,降低常见词的权重,但依然没解决语义问题。

现在的主流做法是用词向量+神经网络。一个经典的TextCNN结构,几行PyTorch代码就能实现:

class TextCNN(nn.Module):
    def __init__(self, vocab_size, embed_dim, num_classes):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        # 用不同尺寸的卷积核捕捉不同范围的语义
        self.convs = nn.ModuleList([
            nn.Conv2d(1, 100, (k, embed_dim)) for k in [2, 3, 4]
        ])
        self.fc = nn.Linear(300, num_classes)  # 100*3=300
        
    def forward(self, x):
        x = self.embedding(x)  # [batch, seq_len, embed_dim]
        x = x.unsqueeze(1)  # 加通道维 [batch, 1, seq_len, embed_dim]
        
        conv_results = []
        for conv in self.convs:
            conv_out = torch.relu(conv(x)).squeeze(3)  # 去掉最后一维
            pool_out = torch.max_pool1d(conv_out, conv_out.size(2)).squeeze(2)
            conv_results.append(pool_out)
            
        x = torch.cat(conv_results, 1)  # 拼接不同卷积核的结果
        return self.fc(x)

这个结构巧妙之处在于,用多个尺寸的卷积核同时捕捉2-gram、3-gram、4-gram特征。比如“电池续航”这种二元词组能被2-gram卷积核捕获,“充电速度快”这种三元组能被3-gram捕获。

实际部署时,建议先用FastText跑个基线。它的字符级n-gram特性对拼写错误、未登录词特别鲁棒:

from fasttext import FastText

model = FastText.train_supervised(
    input="train.txt",
    epoch=25,
    lr=1.0,
    wordNgrams=2,  # 用2-gram特征
    bucket=2000000
)
# 训练快,效果不错,特别适合应急上线

工程实践里的几个坑

  1. 领域适配问题
    通用预训练模型在医疗、法律等专业领域效果会打折。我们之前做医疗文本分类,用通用BERT准确率只有72%,加入10万条医疗文献微调后到了89%。如果资源有限,至少用领域语料训练词向量层。

  2. 类别不平衡处理
    真实场景中“其他”类可能占90%。除了重采样,试试Focal Loss:

class FocalLoss(nn.Module):
    def __init__(self, alpha=0.25, gamma=2):
        super().__init__()
        self.alpha = alpha
        self.gamma = gamma
        
    def forward(self, inputs, targets):
        BCE_loss = F.cross_entropy(inputs, targets, reduction='none')
        pt = torch.exp(-BCE_loss)
        loss = self.alpha * (1-pt)**self.gamma * BCE_loss
        return loss.mean()
  1. 短文本分类
    商品标题、搜索查询这类短文本,词向量效果可能不如传统方法。可以试试SVM+TF-IDF组合,有时候反而更稳定。

  2. 在线服务优化
    BERT虽然效果好,但推理速度慢。可以蒸馏成小模型,或者用CNN/RNN结构。我们线上服务从BERT-base换成蒸馏后的3层Transformer,响应时间从120ms降到28ms,精度只掉了1.2个百分点。


个人经验建议

别一上来就怼BERT。先从FastText或TextCNN跑通流程,确保数据管道没问题。很多bug不是模型问题,而是数据预处理时编码不一致、分词方案冲突这类低级错误。

词向量一定要可视化检查。用TSNE降维后画个散点图,看看“手机”是不是靠近“电话”,“苹果”是不是同时靠近“水果”和“手机”。肉眼观察比指标更直观,能提前发现很多问题。

文本分类的评估指标要选对。多分类别只看准确率,特别是类别不平衡时。F1-score、混淆矩阵、AUC-ROC都看看。我们那个充电宝分类问题,如果早看混淆矩阵,就能发现模型把所有带“电”字的都归到了一类。

最后,模型上线后一定要加降级策略。当分类置信度低于阈值时,走规则匹配或人工审核流程。我们吃过亏——模型更新后有个隐层维度没对齐,线上全乱套,因为没有降级策略,直接导致标签系统瘫痪半小时。

NLP项目,30%时间在模型调优,70%时间在数据清洗和错误分析。多花时间看看模型分错的样本,比调超参有用得多。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐