在使用Scikit-learn做文本特征提取时,CountVectorizer是最常用的工具之一。但它内置的分词规则会主动忽略很多带有数字和符号的片段,例如版本号、产品型号、带连字符的编号等,这在某些业务场景下会造成重要信息丢失。理解其默认行为并改用自定义分词器,是解决这个问题最直接的方式。

一、CountVectorizer为什么忽略数字符号
CountVectorizer在初始化时有一个参数叫token_pattern,它的默认值是正则表达式(?u)bww+b。这个规则的含义是:匹配由字母、数字或下划线组成、且长度大于等于2的“词边界”片段。虽然w本身包含数字,但在实际英文或中文混合文本中,像“V2.0”里的点号“.”不属于w,因此整个字符串无法被完整匹配;而“CPU-99”中的连字符也会切断匹配,最终只可能留下“CPU”和“99”被分开,或者因为长度或边界问题被丢弃。
另外一个容易被忽视的点是,CountVectorizer在调用内部逻辑时,会先按token_pattern做抽取,再统计词频。如果你关心的文本恰好是“Python3.10”“error-404”这类组合,默认配置下它们要么被拆散,要么直接消失。下面是一段演示默认行为的代码:
from sklearn.feature_extraction.text import CountVectorizer docs = ["Python3.10 released", "error-404 not found", "version V2.0 stable"] vec = CountVectorizer() X = vec.fit_transform(docs) print(vec.get_feature_names_out()) # 输出可能类似:['404', 'not', 'found', 'python3', 'released', 'stable', 'version'] # 注意 'python3.10' 'error-404' 'v2.0' 均未被完整保留
从上面的输出可以看到,“python3”被保留但“.10”丢了,“error”和“404”被拆开,“v2”和“0”也被拆开。这种处理对于普通自然语言文本问题不大,但在日志分析、软件版本归类、设备编码统计等任务中,就会让模型完全看不到真实语义单元。
二、自定义分词器的基本思路
解决思路并不复杂:CountVectorizer提供了一个tokenizer参数,允许你传入一个函数,这个函数接收一段字符串,返回切分后的词列表。一旦传入了tokenizer,默认的token_pattern就会失效,控制权完全交给你。你可以用自己的正则去保留数字和符号,也可以用更精细的规则处理特定格式。
例如,如果我们希望保留字母、数字、点号、连字符组成的连续片段,可以写一个简单的正则分词器。下面代码展示了如何定义并应用它:
import re
from sklearn.feature_extraction.text import CountVectorizer
def custom_tokenizer(text):
# 匹配字母数字以及点号连字符下划线,长度至少1
pattern = re.compile(r'[A-Za-z0-9._-]+')
return pattern.findall(text)
docs = ["Python3.10 released", "error-404 not found", "version V2.0 stable"]
vec = CountVectorizer(tokenizer=custom_tokenizer)
X = vec.fit_transform(docs)
print(vec.get_feature_names_out())
# 输出:['404', 'error-404', 'not', 'found', 'python3.10', 'released', 'stable', 'v2.0', 'version']
对比上一节的输出,现在“python3.10”“error-404”“v2.0”都被当作完整词项保留下来了。这样的特征对于后续分类或检索显然更有意义。需要注意的是,自定义分词器返回的列表元素就是最终用于统计的词,因此你可以在函数里做小写化、去停用词等任何处理。
三、更复杂的场景与改进方案
在真实项目中,文本可能同时包含中文、英文、数字和多种符号。如果只用上面的粗粒度正则,可能会把无意义的纯符号串也保留下来。此时可以结合多个正则,或者在分词器里加过滤逻辑。比如只保留“含字母且含数字”或“纯数字带符号”的片段,其余交给默认单词处理。
下面示例展示一个兼顾中英文与数字符号的分词器,它用正则表达式先抽取英文数字组合,再用空格切分中文,并过滤掉过短的无意义符号:
import re
from sklearn.feature_extraction.text import CountVectorizer
def mixed_tokenizer(text):
tokens = []
# 抽取英文、数字、点、连字符组合
en_pattern = re.compile(r'[A-Za-z0-9._-]+')
for m in en_pattern.findall(text):
if len(m) > 1:
tokens.append(m.lower())
# 简单按非中英文字符切分中文词(演示用,生产可接jieba)
cn_parts = re.split(r'[^一-龥]+', text)
for part in cn_parts:
if len(part) >= 2:
tokens.append(part)
return tokens
docs = ["Python3.10发布 error-404", "版本V2.0稳定"]
vec = CountVectorizer(tokenizer=mixed_tokenizer)
X = vec.fit_transform(docs)
print(vec.get_feature_names_out())
# 输出类似:['error-404', 'python3.10', '版本', '稳定', 'v2.0']
这个分词器把“python3.10”“error-404”“v2.0”完整保留,同时也通过拆分拿到了中文词。它的优点是灵活,缺点是中文切分较粗糙。如果中文是重点,建议在函数内部调用jieba等库,再统一把数字符号组合并进去。无论如何,核心机制都是利用tokenizer参数绕过默认token_pattern。
四、使用自定义分词器的注意事项
第一,传入tokenizer后,诸如lowercase参数仍然生效(默认True会在分词后统一小写),但你也可以在函数里自己控制,避免重复处理。第二,自定义分词器会增加一些计算开销,特别是正则复杂或调用外部库时,在大规模语料上要评估性能。第三,词表可能膨胀,因为带符号的组合词变多,可配合max_features或min_df限制。
最后提醒,如果同时还设置了token_pattern和tokenizer,Scikit-learn会以tokenizer为准并给出警告。因此写代码时建议明确只使用其中一个。下表总结了默认与自定义方案差异:
| 方案 | 数字符号处理 | 适用场景 |
|---|---|---|
| 默认token_pattern | 拆散或丢弃 | 普通自然语言文本 |
| 自定义tokenizer | 完整保留可控 | 版本号、编号、日志、混合文本 |
通过自定义分词器,我们能让CountVectorizer真正理解业务里的“数字符号词”,不再漏掉关键特征。只要根据语料特点写好那个小小的函数,文本向量化质量就会有明显提升。
CountVectorizer自定义分词器tokenizer修改时间:2026-08-04 16:51:45