1. 文本向量化的两条路:从独热编码到预训练词嵌入
做文本分类、情感分析或者序列标注的朋友,大概率都卡在同一个地方:模型跑起来了,但效果就是上不去。回头一查,问题往往出在第一步——文本怎么变成向量。我刚开始接触Keras做NLP的时候,最常用的就是Tokenizer加pad_sequences,然后接一个Embedding层从头训练。这套组合拳打小数据集还行,一旦语料稍微复杂点,准确率就原地踏步。后来才明白,问题不在模型结构,而在词向量本身是随机初始化的,模型得从零开始学每个词的含义,数据量不够根本学不明白。
这篇内容就是围绕Keras文本序列处理的第二阶段展开:在完成分词和序列填充之后,如何用预训练的词嵌入来替换随机初始化的Embedding层。说白了,就是让模型站在巨人的肩膀上——用别人在大规模语料上训练好的词向量,直接拿过来当起点。这样做的好处很直接:训练更快、效果更稳、小数据集也能跑出像样的结果。
适合谁来参考?如果你已经会用Keras做基本的文本预处理,知道Tokenizer怎么用、pad_sequences怎么填充,但模型效果一直不理想,那这篇就是写给你的。如果你还没接触过Keras的文本处理,建议先补一下分词和序列化的基础操作,不然直接上预训练词嵌入会有点懵。下面我会从设计思路、核心细节、实操过程到问题排查,一步步拆开讲,尽量把每个“为什么”都说清楚。
2. 为什么预训练词嵌入值得用:原理与选型考量
2.1 从随机初始化到预训练迁移的本质区别
Keras的Embedding层默认行为是随机初始化一个(vocab_size, embedding_dim)的权重矩阵,然后在训练过程中通过反向传播慢慢调整。这个过程本质上是在学每个词的分布式表示,但有个前提:你得有足够多的数据让模型“见够”每个词在不同上下文中的用法。实际项目中,标注数据往往只有几千到几万条,稀有词可能只出现几次,模型根本来不及学好它们的向量。
预训练词嵌入的思路完全不同。它是在大规模无标注语料(比如几十亿词的新闻、百科、网页文本)上,通过某种算法(如Word2Vec、GloVe、FastText)预先训练好每个词的向量。这些向量已经编码了词与词之间的语义关系,比如“国王”和“王后”的向量差异,跟“男人”和“女人”的向量差异很接近。你把它们加载到Keras的Embedding层里,相当于给模型一个高质量的起点,后续微调只需要在具体任务上做小幅调整。
我自己的经验是:在文本分类任务上,用预训练词嵌入替换随机初始化,验证集准确率通常能提升5到15个百分点,具体取决于任务难度和预训练语料与目标领域的匹配度。训练轮数也能减少三分之一左右,因为模型不需要从头学词义了。
2.2 主流预训练词嵌入的选型对比
选哪个预训练词嵌入,得看你的任务语言、领域和计算资源。下面这张表是我用过几种之后的对比,供你参考:
| 词嵌入类型 | 训练语料特点 | 维度典型值 | 优点 | 缺点 |
|---|---|---|---|---|
| Word2Vec | 通用新闻/网页 | 100-300 | 训练快,通用性强 | 对稀有词和形态变化处理一般 |
| GloVe | 维基百科+网页 | 50-300 | 全局共现统计,语义关系好 | 内存占用较大 |
| FastText | 通用+多语言 | 100-300 | 支持子词,能处理未登录词 | 模型文件较大 |
| 领域专用嵌入 | 医疗/法律/金融 | 100-200 | 领域语义匹配度高 | 获取难度大,需自行训练 |
选型时优先考虑目标语言和领域匹配度。做中文任务就用中文预训练词嵌入,做医疗文本就别用新闻语料训练的。维度方面,100到300之间比较常见,太小表达能力不足,太大容易过拟合且加载慢。我一般先用200维试水,效果不够再往上加。
注意:预训练词嵌入文件通常很大(几百MB到几个GB),加载到内存需要一定时间。如果内存有限,可以只加载词表中实际用到的词向量,而不是全量加载。
2.3 冻结还是微调:Embedding层的训练策略
加载预训练词嵌入后,Embedding层有两种用法:冻结(trainable=False)和微调(trainable=True)。冻结意味着词向量在训练过程中不变,只作为固定特征输入;微调则允许模型在任务数据上继续调整词向量。
怎么选?我的经验法则:
- 数据量小(<1万条):先冻结,避免过拟合。如果效果不够,再解冻最后几层微调。
- 数据量中等(1-10万条):可以微调,但学习率要设小一点(比如1e-4),避免把预训练学到的语义关系破坏掉。
- 数据量大(>10万条):微调通常更好,模型有足够数据来调整词向量适应任务。
还有个折中方案:先冻结训练几轮,等模型其他部分稳定了,再解冻Embedding层用很小的学习率继续训练。这样既利用了预训练知识,又能做任务适配。
3. 核心实操:在Keras中加载并使用预训练词嵌入
3.1 准备词嵌入文件与构建词表映射
预训练词嵌入文件常见格式是文本文件,每行一个词加一串浮点数,用空格分隔。第一行可能是词数和维度,也可能直接是词向量。加载时先解析成字典:
import numpy as np def load_embedding(path, dim): embeddings = {} with open(path, 'r', encoding='utf-8') as f: for line in f: parts = line.strip().split() if len(parts) == dim + 1: word = parts[0] vec = np.asarray(parts[1:], dtype='float32') embeddings[word] = vec return embeddings同时,用Keras的Tokenizer在训练语料上构建词表。这里有个关键点:Tokenizer的词表索引从1开始(0留给填充),而预训练词嵌入的键是词本身。所以需要建立一个从词表索引到词嵌入向量的映射矩阵:
from tensorflow.keras.preprocessing.text import Tokenizer tokenizer = Tokenizer(num_words=max_words) tokenizer.fit_on_texts(texts) word_index = tokenizer.word_index embedding_matrix = np.zeros((max_words, embedding_dim)) for word, i in word_index.items(): if i < max_words: vec = embeddings.get(word) if vec is not None: embedding_matrix[i] = vec没找到对应词向量的词,保持全零或者用随机小值初始化。全零的好处是模型能学到“未知词”的统一表示,坏处是如果未知词太多,这部分信息就浪费了。我一般统计一下覆盖率,如果低于70%,就得考虑换词嵌入或者调整分词策略。
3.2 构建Embedding层并加载权重
有了embedding_matrix,构建模型时把它传给Embedding层:
from tensorflow.keras.layers import Embedding, LSTM, Dense from tensorflow.keras.models import Sequential model = Sequential() model.add(Embedding( input_dim=max_words, output_dim=embedding_dim, weights=[embedding_matrix], trainable=False # 先冻结 )) model.add(LSTM(64)) model.add(Dense(1, activation='sigmoid')) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])weights=[embedding_matrix]这个参数是关键,它让Embedding层用预训练权重初始化,而不是随机初始化。trainable=False表示冻结,后续想微调改成True即可。
提示:如果之后要解冻微调,需要重新编译模型,因为
trainable属性的改变不会自动生效。
3.3 序列填充与批次训练的参数设置
文本序列长度不一致,需要统一填充到固定长度。pad_sequences的maxlen选多少?我一般看序列长度的分布,取覆盖95%样本的长度。比如大部分句子在50词以内,就设maxlen=50,超出的截断,不足的补零。
from tensorflow.keras.preprocessing.sequence import pad_sequences sequences = tokenizer.texts_to_sequences(texts) X = pad_sequences(sequences, maxlen=maxlen, padding='post', truncating='post')padding='post'表示在末尾补零,truncating='post'表示超出长度时截断末尾。这两个参数对LSTM类模型影响不大,但对CNN类模型,补零位置会影响卷积窗口,需要根据模型结构选择。
批次大小方面,冻结Embedding时可以用大一点的批次(64或128),微调时用小批次(32)更稳。学习率冻结时用默认的1e-3,微调时降到1e-4甚至1e-5。
4. 完整实操流程:从原始文本到模型训练
4.1 数据准备与分词清洗
假设你有一批文本和对应的标签,第一步是清洗。去掉HTML标签、特殊符号、多余空格,中文还需要考虑是否分词。如果用的是词级别的预训练词嵌入,中文需要先分词(比如用jieba),英文按空格切分即可。
import re import jieba def clean_text(text): text = re.sub(r'<[^>]+>', '', text) text = re.sub(r'[^\w\s]', '', text) text = text.strip().lower() return text def tokenize_chinese(text): return ' '.join(jieba.cut(text))清洗后统一分词,然后用Tokenizer构建词表。这里有个细节:Tokenizer默认按空格分词,中文分词后词之间用空格连接,正好适配。
4.2 加载预训练词嵌入并构建嵌入矩阵
这一步是核心。先加载词嵌入文件,再遍历词表构建矩阵。我通常会把嵌入矩阵缓存成.npy文件,避免每次训练都重新加载:
import numpy as np import os def build_embedding_matrix(word_index, embeddings, max_words, embedding_dim): matrix = np.zeros((max_words, embedding_dim)) hit = 0 for word, i in word_index.items(): if i < max_words: vec = embeddings.get(word) if vec is not None: matrix[i] = vec hit += 1 print(f'词表覆盖率: {hit / min(len(word_index), max_words):.2%}') return matrix if os.path.exists('embedding_matrix.npy'): embedding_matrix = np.load('embedding_matrix.npy') else: embedding_matrix = build_embedding_matrix(word_index, embeddings, max_words, embedding_dim) np.save('embedding_matrix.npy', embedding_matrix)覆盖率低于70%时,我会检查分词是否合理,或者换一个覆盖更全的词嵌入。有时候把未登录词统一映射到一个特殊标记,比留零向量效果更好。
4.3 模型构建、训练与微调策略
模型结构根据任务定。文本分类常用LSTM或BiLSTM加池化,序列标注用BiLSTM+CRF。这里以二分类为例:
from tensorflow.keras.layers import Bidirectional, GlobalMaxPooling1D, Dropout model = Sequential() model.add(Embedding(max_words, embedding_dim, weights=[embedding_matrix], trainable=False)) model.add(Bidirectional(LSTM(64, return_sequences=True))) model.add(GlobalMaxPooling1D()) model.add(Dropout(0.5)) model.add(Dense(64, activation='relu')) model.add(Dense(1, activation='sigmoid')) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) history = model.fit(X_train, y_train, validation_split=0.2, epochs=10, batch_size=64)冻结训练几轮后,如果验证集指标不再提升,可以解冻Embedding层微调:
model.layers[0].trainable = True model.compile(optimizer=tf.keras.optimizers.Adam(1e-4), loss='binary_crossentropy', metrics=['accuracy']) history_fine = model.fit(X_train, y_train, validation_split=0.2, epochs=5, batch_size=32)微调时学习率一定要小,否则预训练学到的语义关系会被破坏。我试过用1e-3微调,结果比冻结还差,后来降到1e-5才稳定提升。
5. 常见问题与排查技巧实录
5.1 词表覆盖率低怎么办
覆盖率低通常有三个原因:分词方式不匹配、词嵌入领域不匹配、词表太大。排查顺序:
- 检查分词结果,看是否有大量无意义片段。中文分词用jieba时,可以加自定义词典。
- 换一个覆盖更全的词嵌入,或者用子词级别的FastText。
- 减小
max_words,只保留高频词,低频词统一用未知标记。
我遇到过一次覆盖率只有50%,后来发现是词嵌入文件编码不对,读进来全是乱码。所以加载后先打印几个词看看是否正常。
5.2 加载词嵌入后模型不收敛
可能原因和解决思路:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 损失不下降 | 学习率太大或太小 | 冻结时用1e-3,微调用1e-4到1e-5 |
| 准确率震荡 | 批次太小或数据未打乱 | 增大批次,检查shuffle=True |
| 验证集远差于训练集 | 过拟合 | 加Dropout,冻结Embedding,减少参数量 |
| 微调后效果变差 | 学习率太大破坏预训练权重 | 降低学习率,只微调最后几层 |
我踩过最坑的一次是微调时忘了重新编译模型,trainable改了但优化器还是旧的,结果训练完全没变化。所以改完trainable一定要重新compile。
5.3 内存不足与加载速度优化
预训练词嵌入文件动辄几个GB,全量加载到内存可能吃不消。优化方法:
- 只加载词表中出现的词向量,跳过其他词。加载时先读词表,再过滤。
- 用
np.load的mmap_mode参数做内存映射,按需读取。 - 把嵌入矩阵转成
float32,别用float64,内存减半。
加载速度方面,把解析后的嵌入矩阵缓存成.npy或.h5文件,下次直接加载,省去文本解析时间。我实测一个300万词、200维的词嵌入,文本解析要3分钟,加载.npy只要10秒。
5.4 微调时如何避免灾难性遗忘
灾难性遗忘指微调后模型忘了预训练学到的通用语义。避免方法:
- 用很小的学习率(1e-5到1e-4)。
- 只解冻Embedding层的一部分维度,或者只微调高频词。
- 在微调数据中加入一部分通用语料,混合训练。
- 用早停,验证集不提升就停。
我一般先冻结训练到收敛,再解冻微调2到3轮,每轮都监控验证集。如果验证集下降,立刻回滚到冻结版本。
6. 几个容易被忽略的细节与个人经验
6.1 特殊标记的处理
Tokenizer默认用0做填充,但预训练词嵌入里通常没有对应0的向量。构建嵌入矩阵时,索引0保持全零即可,模型会学到“填充位置”的表示。另外,未知词可以用一个专门的标记(比如<unk>),在词表中给它分配索引,并从词嵌入里找对应向量。如果词嵌入没有<unk>,就用所有词向量的均值代替,比全零更合理。
6.2 词嵌入维度与模型容量的匹配
词嵌入维度不是越大越好。100维适合小数据集和简单任务,200到300维适合中等数据集,300维以上需要更多数据才能发挥优势。如果Embedding层后面接的是LSTM,维度太大还会拖慢训练速度。我一般从200维起步,根据验证集效果调整。
6.3 多语言与跨领域任务的注意事项
做多语言任务时,选多语言预训练词嵌入(比如在多种语言语料上训练的),或者每种语言单独加载。跨领域任务(比如用新闻词嵌入做医疗文本分类)效果会打折扣,这时候要么找领域词嵌入,要么在目标领域语料上继续训练词嵌入(用Word2Vec或FastText),再加载到Keras里。
6.4 保存与复用嵌入矩阵
嵌入矩阵构建一次就可以反复用。保存成.npy文件,下次直接np.load。如果换了词表或max_words,需要重新构建。我习惯把词表、嵌入矩阵、maxlen这些配置一起保存,方便复现。
import pickle config = { 'max_words': max_words, 'maxlen': maxlen, 'embedding_dim': embedding_dim } with open('config.pkl', 'wb') as f: pickle.dump(config, f)这样下次训练新模型时,直接加载配置和嵌入矩阵,不用重新处理文本。
6.5 实测效果对比与调参建议
我在一个中文情感分析数据集上做过对比:随机初始化Embedding的验证集准确率是82.3%,加载预训练词嵌入后升到89.7%,微调后到91.2%。训练轮数从15轮降到8轮。调参时优先调学习率和批次大小,其次调LSTM单元数,最后调Dropout比例。预训练词嵌入本身的质量对结果影响最大,选型时多花点时间值得。
最后分享一个小技巧:如果找不到合适的预训练词嵌入,可以用自己的无标注语料训练一个Word2Vec模型,再加载到Keras里。虽然效果不如大规模预训练的,但比随机初始化强很多,而且领域匹配度更好。训练Word2Vec用gensim几行代码就能搞定,语料有个几十MB就能出不错的结果。