# 标签分类理论

LLMS 索引： [llms.txt](/llms.txt)

---

最近在某业务中需要设计一套标签管理系统。在对现有标签进行整理的过程中，倒腾出了这套理论。

## 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](https://codex.wordpress.org/Database_Description)
