标签分类理论
最近在某业务中需要设计一套标签管理系统。在对现有标签进行整理的过程中,倒腾出了这套理论。
0. 标签的定义:标签分类学(Taxonomy)
对于标签(tag),很难列出一个公认的定义,指明这个概念的种差与属概念。 所以为了把握这个概念,就需要采取定义另一种办法:分类与枚举。
要解决的第一个问题是,有哪些类型的标签?如何对标签进行分类? 首先不妨对“如何分类”本身进行分类:分别从“形式”与“内容”上考察标签的分类。
1. 标签的形式分类
标签的形式是标签分类最主要的依据。 我们可以列出一些常见或者不常见的的“标签”样例:
性别标签:女
年龄标签:23
体重标签:90.6
偶像标签:阿西莫夫
最近到过的城市标签:['北京','青岛','成都']
兴趣标签:['滑雪','旅游','吃']
三围标签:[100,100,100]
上年消费额标签:[5250.12,6873.23,1232.12,3231.23,...,2321.24]
网站浏览偏好标签:{"问答类":0.55, "交友类":0.75, "旅游类":0.82, "团购类":0.32,"电商":0.78,...}
手机品牌偏好标签:{"iphone7":0.99, "iphone5":0.35, "小米3":0.12,...}
预测游戏分数标签:{0 : 0.2, ..., 100 : 0.003, ..., 198 : 0.01, 199 : 0.01, 2100 : 0.005,...}
预测年龄标签:30 : <置信度0.72>
通过观察,可以发现一些规律:
1.1 从标签的组织形式上看
- 通常意义上的标签是单值标签,或称原子标签。其取值是一个独立的值。如
女
,23
,90.6
。 - 一部分标签是多值标签,多个原子标签作为一个整体,形成一个标签。例如微博上人们用来描述自己的关键词列表:例如:
['90后','处女座','么么哒']
。 - 为多值标签的每个原子标签添加关联权值就得到了权值标签。例如对不同品牌手机的喜好程度:
{"iphone7":0.99, "iphone5":0.35, "小米3":0.12,...}
- 单个的原子标签带有权值也是常见的事情,例如给出一个预测年龄及其置信度。这种单kv结构也用权值标签来表示就会显得奇怪与累赘。因此应当单独作为一类,称为单权标签。例如:
[30 , 0.72]
可以表示预测年龄30岁,置信度0.72。
结论:
从标签的组织形式上看,标签可以分为四类:单值标签,单权标签,多值标签,多权标签。
于是可以得到两个基本正交的维度:是否为多值标签, 是否带有权值。
这四种标签结构类型,单值标签、多值标签、多权标签,恰好与JSON的三种Primitive Type:atomic
, array
, object
相对应。而特殊的单权标签可以映射为长度为2的array
。
1.2 从标签的原子类型上来看
我们知道,计算机(x86,通用计算机)的实现本质上只提供了整型与浮点两种原子数据类型。指针,单字符,布尔,浮点数都属于数值类型,极其常用的字符数组可以看做字符串类型,那么逻辑上其实我们就只有两种原子数据类型:数值(Numeric)
与字符串(String)
。
所有原子标签只有数值
与字符串
这两种简单分类的想法当然很美好。但出于现实的需求约束的考虑(比如就是有离散标签与连续值标签的区分,ODPS区分BIGINT和DOUBLE),我们还是会将数值细分为整型与浮点,所以用于原子标签的类型变为了三种:整型
、浮点
、字符串
。
另一方面,对于权值标签(单权,或者多权),除了原子标签的值具有类型,其权值也应当有一个合适的类型。强制其类型为数值是一个合理且合适的约束。更具体一些,将权值实现为Double
是相当合理的选择。
标签的原子类型和结构类型并不完全正交,这是出于一些技术上的约束。很多语言中的关联数组(Map)都可以使用各种类型作为Key(int
,string
,double
)。然而JSON规范中只有string
才可以作为Object的key。这并不是无法调和的问题:整型可以安全地通过序列化为string
作为key。但浮点数
的不精密性在序列化中会造成许多意想不到的麻烦,所以多值标签的原子类型不能为浮点。
结论:
从原子类型分类上看:标签可以分为整型
,浮点
,字符串
。
1.3 从对整型原子类型的解释方法上看
在2.1.2中,我们对标签的原子类型进行了分类。但我们必须考虑另外一种生产实践中最最常见的标签分类:枚举标签。枚举标签通常在形式上用一个整型表示,同时提供一个从整型值到字符串的枚举字典用于解释这个整型值。
例如:
# 性别标签字典
gender_dict = {0:'男', 1:'女', 2: '人妖'....}
# 性别标签取值
0 # 一个用于表示男性的单值枚举标签
[0, 0, 1, 0] # 一个用于表示家庭性别构成的多值枚举标签
{0 : 0.1, 1: 0.4} # 一个用来表示预测性别+置信度 或者 性取向+倾向度 的多值枚举标签。
再比如:
# 省份对照字典
province_dict = {11:'北京',12:'天津',13:'河北',......}
# 省份取值标签
13 # 单值枚举标签,我到河北省来!
{'11': 0.76, '13':0.1} # 多值枚举标签,例如用户下一步预测作案地点概率+可行性。
另外在某种意义上,布尔标签就是一种特殊的枚举标签,其枚举字典为:{0:False,1:True}
,完全可以自然地纳入枚举标签的体系中,甚至通过枚举标签,还可以实现所谓的Nullable boolean
,为布尔标签添加更多的语义。
所以,对于整型原子类型的解释方法也可以成为一个标签分类的维度。即是否为枚举标签
。但是这个维度和2.1.3中原子标签类型的维度高度相关(因为当原子类型为整型时,本维度才有效)。所以这两个维度应当合二为一。
FAQ:
-
枚举与整型的区别在哪里,即什么时候用整型什么时候用枚举? 很简单,取值可以穷尽、数目合理、变动不频繁的时候用枚举。例如,城市代码就是一个很合适的枚举标签:它可以穷尽,数量级完全可接受,虽然有可能变动,但几率和订正成本是可以接受的。另一方面,另一方面,一个人的头发数目肯定可以用一个整数表示,但一来无法穷尽,二来数目巨大,明显不适合作为枚举标签。
-
枚举与字符串的区别? 例如,用户使用的手机品牌,似乎可以用一个单值字符标签标示,也可以用枚举来实现。但它更适合使用
字符串
而非枚举
。因为手机品牌并不是数目固定的,会不断地有品牌诞生与消逝。在这种情况下,枚举字典的频繁变化将对于标签使用带来诸多不便。 -
枚举标签的特殊之处? 枚举标签需要维护一张标签字典表,用于维护从枚举项ID到枚举项Name的映射关系。多个枚举标签的字典可以在同一张表中维护。同时,枚举标签可以具有层次关系。例如"城市枚举标签"就可以有上层标签:“省份枚举标签”,具有层次关系的枚举标签可以通过提供枚举项映射,方便地实现上卷与下钻。
-
为什么不采用字符串作为枚举项ID? 在绝大多数语言中枚举都是默认以整型实现的。整型ID相比字符串ID具有极大的性能优势与简洁性。
结论:
按照原子标签的取值类型与解释方式进行分类,我们可以得到一个维度:标签原子类型
。
该维度的取值有4种: 枚举
,整型
,浮点
,字符串
1.4 形式分类小结
由上述可知,从标签的形式上,我们获得了两个大的,基本正交的分类维度:
- 组织形式:{
单值标签
,单权标签
,多值标签
,多权标签
} - 原子类型:{
枚举标签
,整型标签
,文本标签
,浮点标签
, }
除了浮点多权标签
不是合理的组合之外,其他共计 4 x 4 -1 = 15
种组合。
即标签从形式上可以分为15个类型,恰好在4个bit的表示范围内。
按照标签原子类型的出现频率,可以为最常出现的标签类型分配靠前的编码。 因为最常见的标签都是单值标签,将标签结构类型的位域放在标签原子类型的位域之前是合理的设计。 枚举标签是数目最多的标签,整型其次,字符串标签有一些,浮点标签则比较少见。 所以,可以为标签的形式类型分配如下的编码:
1.4.1 标签结构类型字段
结构 | 标记 | 说明 |
---|---|---|
单值标签 | 0x00 | 取值为单一原子类型相应值 |
单权标签 | 0x01 | 取值为单一原子类型及其权值,采用长度为2的数组表示 |
多值标签 | 0x10 | 取值为同种原子类型组成的列表 |
权值标签 | 0x11 | 取值为同种原子类型组成的字典,key只能为string或string(bigint) |
1.4.2 标签原子类型字段
结构 | 标记 | 说明 |
---|---|---|
枚举标签 | 0x00 | 实际为Bigint类型,默认类型,需要对照类型字典解读 |
整型标签 | 0x01 | 整型数值原子标签 |
文本标签 | 0x10 | 字符串原子标签 |
浮点标签 | 0x11 | 浮点数数值原子标签 |
1.4.3 标签形式分类一览
类型ID | 英文代号 | 名称 | 结构ID | 结构名 | 原子ID | 原子名称 | 存储 |
---|---|---|---|---|---|---|---|
0 | atom-enum | 单值枚举 | 0 | 单值 | 0 | 枚举 | int |
1 | atom-int | 单值整型 | 0 | 单值 | 1 | 整型 | int |
2 | atom-text | 单值文本 | 0 | 单值 | 2 | 文本 | text |
3 | atom-float | 单值浮点 | 0 | 单值 | 3 | 浮点 | float |
4 | pair-enum | 单权枚举 | 1 | 单权 | 0 | 枚举 | json |
5 | pair-int | 单权整型 | 1 | 单权 | 1 | 整型 | json |
6 | pair-text | 单权文本 | 1 | 单权 | 2 | 文本 | json |
7 | pair-float | 单权浮点 | 1 | 单权 | 3 | 浮点 | json |
8 | list-enum | 多值枚举 | 2 | 多值 | 0 | 枚举 | json |
9 | list-int | 多值整型 | 2 | 多值 | 1 | 整型 | json |
10 | list-text | 多值文本 | 2 | 多值 | 2 | 文本 | json |
11 | list-float | 多值浮点 | 2 | 多值 | 3 | 浮点 | json |
12 | dict-enum | 多权枚举 | 3 | 多权 | 0 | 枚举 | json |
13 | dict-int | 多权整型 | 3 | 多权 | 1 | 整型 | json |
14 | dict-text | 多权文本 | 3 | 多权 | 2 | 文本 | json |
这里需要提一下的是,标签形式分类与其存储类型的关系:
存储上,单值标签采用Bigint
,Double
,String
存储。单权标签采用长度固定为2的数组[value,weight]
存储,多值标签采用数组存储[value1,value2,...]
,多权标签采用对象{value1: weight1,...}
存储,且当原子类型为整型与枚举时,其中的value
应当存储其字符串序列化形式以符合JSON对key类型的要求。
从结果上看,所有单值标签都直接以其对应类型进行序列化存储。其余所有标签都采用JSON序列化的方式存储。
下面给出每种标签的样例:
1.4.4 标签形式分类样例表
id | title | storage | sample |
---|---|---|---|
0 | 单值枚举 | int | 性别标签:1 {“0”:“男”, “1” :“女”} |
1 | 单值整型 | int | 年龄:23 |
2 | 单值文本 | text | 喜爱小说:“百年孤独” |
3 | 单值浮点 | float | 体重:60.13 |
4 | 单权枚举 | json | 预测性别:[1, 0.99] |
5 | 单权整型 | json | 预测年龄:[23, 0.99] |
6 | 单权文本 | json | 电视剧-喜爱度:[“星际迷航”, 9.8] |
7 | 单权浮点 | json | 预测体重:[60.13, 0.78] |
8 | 多值枚举 | json | 闹钟设置:[1, 2, 3, 4, 5] |
9 | 多值整型 | json | 三围:[100, 100, 100] |
10 | 多值文本 | json | 喜爱电视剧:[“星际迷航”, “绝命毒师”, “是的,大臣!"] |
11 | 多值浮点 | json | 月度消费记录:[6379.13, 6378.24, 6356.12] |
12 | 多权枚举 | json | 闹钟设置概率分布:{“1”:0.98, “2” :0.75, “3” :0.75, “4” :0.5, “5” :0.3} |
13 | 多权整型 | json | 幸运数字 - 喜爱度:{“7”:0.32, “5” :0.63} |
14 | 多权文本 | json | 网站浏览偏好标签:{“问答类”:0.55, “交友类” :0.75} |
2. 标签的内容分类
标签按内容性质分类的方式,相比形式分类显得十分多样。可以纯粹的从标签的取值特性上分类(Nullable,权值是否归一化,etc…),也可以从标签的来源场景(移动端,PC端),标签的所有权(私有,内部,群组,公司),标签的规模,标签的依赖,标签的ID类型,或者前端展示时采用的层级类目,等等很多维度上进行分类。
形式分类会决定标签的展现形式,但内容分类并没有这种影响。所以按内容分类的结果更适合作为描述字段而非放入类型字段。换句话说,与其把内容分类称为分类,倒不如称之为可以动态添加的枚举属性更为合适。
但是对于内容分类,我们仍然需要进一步的考察。标签内容分类可以进一步细分为: 按标签固有属性分类,和按人为用途分类。属于标签固有属性的,适合放入标签元数据表中作为一个字段。而属于人为用途划分的,因为需求可能会频繁地发生变化。所以需要提供一种在不改变数据库Schema的前提下支持动态增添分类体系的机制来实现这一需求。本文建议采用类似"WordPress"的Taxonomy概念实现这样的动态分类体系。
2.2 标签动态分类体系的设计
为了提供适应这种变化需求的灵活性,可以考虑构建一张分类体系表(tag_taxonomy)、一张分类项表(tag_term)、一张分类表(tag_classification)。动态的实现分类体系的增添。如果需要实现带有层次结构的分类体系,只需要在分类项表中为每个分类项维护父条目字段即可。
举个例子,如果我们需要动态添加一个"公私"分类。首先需要在分类体系表里注册这种分类体系:“标签公私分类体系”。然后在分类项表中添加"公有”、“私有"两个分类项,并通过外键引用分类体系表中的标签公私分类体系。最后在标签分类表中,通过外键将具体的标签与分类项相关联。
2.3 内容小结
对于标签的内容分类:
- 标签的固有性质适合作为标签表的字段出现
- 标签的人为分类适合使用动态分类体系通过外键引入。
一种可行的动态分类实现Schema: WordPress Database Description