naive_bayes (NAIVE_BAYES) 字典使用多项式 朴素贝叶斯 模型对文本进行分类,这是文本分类中的标准事件模型:它根据输入中各个 n-gram 在每个类别中出现的频次为其评分。你需要提供一张按类别统计的 n-gram 计数 表,字典会在加载时一次性将其编译为模型,随后用该模型对你传入的任意文本进行分类。
它适用于快速、轻量级的文本分类任务,例如情感分析、topic 或垃圾信息标注,以及语言或文字系统检测。
你可以使用以下三个函数查询该字典:
naiveBayesClassifier返回预测的类别 ID。naiveBayesClassifierWithProb返回预测的类别及其概率。naiveBayesClassifierWithAllProbs返回所有类别及其概率。
dictGet 也能用于分类 (参见说明) 。还有一个函数 naiveBayesNgrams 不执行分类——它会以与该字典相同的方式将文本拆分为 n-gram,因此你可以从原始文本构建训练数据 (参见从原始文本构建训练数据) 。
快速入门
n = 1) 模型。
1. 创建一个源表,用于存储各类别的 n-gram 计数:
1) 和负类 (0) 中各自的出现频次:
NAIVE_BAYES 布局的字典:
PRIMARY KEY ngram 将 ngram 列设为键——但对 NAIVE_BAYES 字典来说,这个“键”是你传入用于分类的文本,而不是可供查找的存储值 (参见 字典结构) 。LAYOUT 用于配置模型:class_attribute 'class_id' 将 class_id 标记为类别标签 (因此,另一个属性 count 表示每个类别的出现次数) ,n 1 使用 unigram,mode 'token' 会将文本拆分为按空白分隔的词 (参见 布局参数) 。
4. 分类 — naiveBayesClassifier 返回类别 id:
1 对应正类,这是根据我们在步骤 2 中插入的训练数据得出的。
0 对应负类。
通过 dictGet 也能得到相同的结果:
0 (负类) ,概率为 0.64。
naiveBayesClassifierWithAllProbs 返回所有类别,并按可能性从高到低排序,各类别概率之和为 1.0——此处负类为 0.64,正类为 0.36。
工作原理
(n-gram, class, count) 观测值。字典加载时,这些行会被一次性编译进模型中。重复的 (n-gram, class) 行会被累加,count = 0 的行则会被忽略。
分类 (在查询时) 。 要对字符串进行分类,模型会:
- 根据
mode和n将其拆分为 n-gram (请参见标记化模式) 。 - 结合类别先验以及输入中各个 n-gram 在该类别中出现的频次,为每个类别打分。
- 按得分对类别排序。得分最高的类别就是
naiveBayesClassifier返回的预测结果;naiveBayesClassifierWithProb和naiveBayesClassifierWithAllProbs还会返回概率——分别是该类别的概率,或全部类别的概率。
alpha,用于平滑处理。平滑可以防止模型仅仅因为某个 n-gram 在训练时未出现在某个类别中,就给该类别打零分。较小的 alpha 会让模型更依赖训练数据,因此某个类别的得分可能远高于其他类别;但当训练数据较少或分布不均时,也可能使模型过于敏感。较大的 alpha 会降低 n-gram 计数的影响,因此不同类别的得分会更接近。如果 alpha 非常大,n-gram 信息几乎不起作用,得分主要由类别先验决定 (下一段会介绍) 。
第二个因素是类别先验——也就是模型在查看文本之前,对每个类别可能性的预先假设。它相当于在考虑任何 n-gram 之前,每个类别已有的初始得分,因此先验越高,该类别就越可能被预测出来。其设置方式取决于 priors_mode。默认情况下 (proportional) ,训练数据中 n-gram 总数更多的类别会以更高的初始得分起步。使用 uniform 时,每个类别的起点都相同,因此结果仅由 n-gram 决定。使用 explicit 时,你可以自行设置每个类别的初始值。请参见先验模式。
如果某个 n-gram 从未在训练数据中出现过,它会被忽略:它不属于模型的词汇表,因此既不会对任何类别有利,也不会对任何类别不利。
该算法遵循用于文本分类的多项式朴素贝叶斯模型;请参见 Manning, Raghavan & Schütze, Introduction to Information Retrieval, ch. 13 (Text Classification and Naive Bayes)。
字典结构
NAIVE_BAYES 字典具有固定的结构:
PRIMARY KEY是单个String列——即 n-gram。在查询时,这个”key”是你传入用于分类的文本,而不是存储的查找键。- 此外,必须声明恰好两个无符号整数属性:类标签和出现次数。类别 ID 在内部始终使用
UInt32,因此类标签必须能放入UInt32(最大为4294967295) ,即使你将其属性声明为UInt64也是如此。超出范围的值会在字典加载时被拒绝,而不是在创建字典时。声明的类型也是一样:如果源类别 ID 或计数无法放入所声明的属性类型,加载会失败,而不会被静默截断。 class_attribute布局参数用于指定哪个属性是类标签;另一个属性会自动视为计数。这两个属性可以按任意顺序声明。
(n-gram, class) 一行,表示该 n-gram 在该类中出现了多少次。你可以通过对语料库进行标记化并对结果分组来生成这些计数,可以在你自己的训练管道中完成,也可以在 ClickHouse 中基于原始已标注文本完成 (参见 Build training data from raw text) 。字典只使用这些计数。
更新模型。 由于该模型是由表支撑的字典,因此可通过更新表并重新加载来重新训练:
布局参数
你可以使用
CREATE DICTIONARY DDL (如上面的快速入门所示) 或在 XML 配置文件中定义字典;关于该文件应放在哪里,请参见字典布局。下面的示例设置了所有布局选项,方便你查看完整配置——只有 class_attribute、n 和 mode 是必需的,其余项的默认值见上表。在配置文件中,先验会写成重复的 prior 元素 (每个类一个,如下所示) ;byte 和 codepoint 的填充标记写成数字 (配置无法承载原始字节) ;而 token 字面量会在需要时进行 XML 转义,因此 <s> 会变成 <s>。
- DDL
- 配置文件
标记化模式
mode 决定什么是“标记”,因此也决定 n-gram 的具体形式。源 n-gram 必须由相同的 mode 和 n 生成。
byte— 每个标记都是单个字节;不假定输入为 UTF-8。设n = 2,'abc'会生成字节二元组'ab'、'bc'。适用于 对任意字节序列进行语言或编码检测,以及任何子字符层面特征很重要的数据。通常与n >= 2搭配使用。codepoint— 每个标记都是一个 Unicode 码点;输入会按 UTF-8 解释。设n = 1,'café'会生成码点'c'、'a'、'f'、'é'。适用于 文字系统和语言检测,以及在空白词边界不可靠时的短文本或 CJK 文本。 (源 n-gram 必须是有效的 UTF-8;查询输入会以宽松方式解码——参见 说明。)token— 每个标记都是一个由 ASCII 空白字符 (空格、制表符、换行符、回车符、换页符、垂直制表符;连续出现会合并为一个分隔符) 分隔的单词。非 ASCII 的 Unicode 空白字符,如U+00A0(不间断空格) 或U+2003(em 空格) ,不会作为分隔符,而是保留在标记内部。只有空白字符会触发拆分——不会转为小写,也不会去掉任何字符——因此'Hello, World!'会变成标记'Hello,'和'World!'(逗号、!和大写字母都会保留) ,在n = 2时,它们会组成唯一的二元组'Hello, World!'。适用于 以空格分词的语言中的词级分类——情感、topic、垃圾信息、句子语言识别。
先验模式
priors_mode 用于选择如何设置先验。
-
proportional(默认) — 每个类的先验与其在训练数据中的 n-gram 总计数成正比——也就是该类count列的总和,而不是其行数或训练文档数——因此,出现越频繁的类,初始概率就越高。在以下情况下选择它:训练时各类的占比 (按 n-gram 总计数计算) 与您预期在查询时出现的频率一致。无需额外提供任何内容——它会根据源计数自动推导得出。 -
uniform— 开始时每个类的可能性都完全相同,因此没有哪个类会先天占优,预测结果完全由输入中的 n-gram 决定。在以下情况下选择它:各类分布均衡,或者训练频率并不能反映每个类在查询时的实际出现频率。无需提供任何内容。 -
explicit— 您通过priors [(0, 0.6), (1, 0.4)]显式提供先验:每个类对应一个(class, probability)对,每个概率都大于 0 且不超过 1,并且总和为1.0。在以下情况下选择它:您知道真实的基础比例,并且它们与训练数据不同——例如,即使训练集是均衡的,生产环境流量中也只有 1% 是垃圾信息。请根据各类在真实场景中的预期占比来计算它们。
边界标记 (padding)
n > 1 时才有意义,因为它能让模型利用文本开头和结尾的信号,从而提高准确性。
为什么有帮助。 当 n > 1 时,文本中间的 n-gram 同时具有完整的左侧和右侧上下文,但第一个和最后一个标记则没有。加入边界标记后,会生成用于表示“文本开始”和“文本结束”的 n-gram,这样模型就能学习与位置相关的模式——例如,某个词在消息开头时特别有辨识度,或者某个字符在词语末尾时更常见。
你必须这样做:
- 分别决定两侧是否启用。
start_token和end_token是相互独立的——可以只设置其中一个、同时设置两个,或者两个都不设置。空值表示该侧不进行填充。 - 选择不常见的值,避免与真实数据冲突,例如
byte使用0x01/0xFF,codepoint使用U+10FFFE/U+10FFFF,或token使用<s>/</s>。 - 用相同的填充方式生成训练 n-gram。 字典会对查询输入进行填充,但绝不会对你的源数据做填充,因此边界标记必须预先包含在你加载的 n-gram 中。要最简单地确保两者一致,可以用
naiveBayesNgrams构建源数据,并传入与 layout 相同的start_token和end_token(以及n和mode) ——它生成的正是字典在查询时产生的带填充 n-gram。
-
byte— 表示字节值的数字,可用十进制或0x十六进制表示 (因此'1'和'0x01'相同) : -
codepoint— 表示 UTF-8 码点的数字,可用十进制或0x十六进制表示 (因此'1114110'和'0x10FFFE'相同) : -
token— 标记字符串的字面值:
从原始文本构建训练数据
naiveBayesNgrams 函数将其拆分为 n-gram。为它提供与字典 布局 相同的 n、mode、start_token 和 end_token,它就会精确生成字典所需的 n-gram,因此训练数据会与模型在查询时看到的内容一致。
给定一个由 (class_id, text) 行组成的表,只需一次 GROUP BY 就能构建出 (ngram, class_id, count) 输入:
training_data 现在可作为 NAIVE_BAYES 字典的有效源 (这里使用的是标记 unigram;请调整 n 和 mode 参数以匹配你的布局) 。该字典会完全按输入原样对查询内容进行分词,因此如果训练文本是小写,而查询文本不是小写,它们的 n-gram 就不会匹配,模型准确性也会受到影响。
先验和文档计数
proportional 先验 (默认值) 是按每个类别的n-gram 总数加权的,而不是按其文档数加权。如果你想使用经典的文档频率先验 (documents_in_class / total_documents) ,请从原始 docs 表中计算,并通过 priors_mode 'explicit' 传入:training_data 创建字典,传入上面计算出的显式先验,并对新的评论进行分类:
1 表示正类,0 表示负类,因此这两条评论都被正确分类。
更多示例
n = 2,mode 'byte';类 0 = 由字母 a–d 构成的字符串,类 1 = 字母 x–z) :
n = 1,mode 'codepoint';类 0 = Latin,1 = Cyrillic) :
store_source 读回训练数据:
n = 2、mode 'codepoint';类 0 = 英语,1 = 西班牙语) 。训练用的 n-grams 通过 naiveBayesNgrams 基于原始单词构建,而边界标记——同时传递给该函数和 布局——可让模型利用每个单词的首尾字母:
注意事项
- 计算型字典语义。 这是一个计算型字典:
dictGet(dict, '<class_attribute>', text)会对text进行分类 (键是待分类的输入,而不是存储的键) ,count属性无法查询,且dictHas始终返回1。 - 加载时的源数据验证。 每个源 n-gram 都必须与配置的
n和mode匹配 (在codepoint模式下,还必须是有效的 UTF-8) ;只要不匹配,加载就会失败。由于零计数行会被忽略 (参见工作原理) ,如果源数据为空,或只包含零计数行,就没有可用于训练的数据,因此会加载失败。 - 查询时的分词较为宽松。 与源数据验证不同,查询输入永远不会被拒绝。在
codepoint模式下,无效的 UTF-8 字节会按尽力而为的方式解码,而不会导致查询失败;在token模式下,只有 ASCII 空白字符才会分隔单词 (如U+00A0这类 Unicode 空白仍会保留在标记内部) 。即使输入格式不合法,也仍然会进行分类——通常会依据先验概率,因为其中的 n-grams 不会与已训练的内容匹配。