0%

深度学习:文本数据的表示方法

我们知道,在深度学习中,神经网络模型只能处理数值张量,所以在处理文本时,不能直接将原始文本作为输入,我们需要一种将文本表示为数值张量的方法,而这转换的过程,就是文本向量化。

《Python深度学习》笔记

文本向量化

文本向量化有许多种形式,但都遵循相同的流程:文本规范化、词元化、建立词汇索引。

文本规范化

文本规范化旨在消除你不希望模型处理的那些编码差异,从而把文本统一格式。通常的做法有:把所有字母转小写、全角符号转半角符号、去掉多余空格和特殊字符等。

规范化后,模型将需要更少的训练数据,并且具有更好的泛化效果。比如,在没有规范化前,模型会把”Love”、”love”等当成 3 个不同的词,每个词分到的训练样本会变少,训练的效果也会产生差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""
中文文本规范化示例
- 全角 → 半角
- 大小写统一(针对英文)
- 去除 HTML、URL、表情等噪声
- 统一/去除标点
- 合并多余空白
"""

import re
import unicodedata

# ---------- 辅助函数 ----------
def strQ2B(uchar: str) -> str:
"""
全角字符转半角(全角空格转半角空格,其他字符统一减 0xFEE0)。
"""
# 采用 Unicode 标准的全/半角映射
inside_code = ord(uchar)
# 全角空格(U+3000)单独处理
if inside_code == 0x3000:
return chr(0x0020)
# 其他全角字符范围:0xFF01-0xFF5E
if 0xFF01 <= inside_code <= 0xFF5E:
inside_code -= 0xFEE0
return chr(inside_code)


def normalize_text(text: str) -> str:
"""
对中文文本进行规范化处理,返回干净的字符串。
"""
# 1️⃣ 全角 → 半角
text = "".join(strQ2B(ch) for ch in text)

# 2️⃣ 小写(仅针对英文字符)
text = text.lower()

# 3️⃣ 去除 HTML 标签、URL、邮箱等噪声
# - 简单的 HTML 标签移除
text = re.sub(r"<[^>]+>", " ", text)
# - URL 移除(http/https/ftp)
text = re.sub(r"https?://\S+|ftp://\S+", " ", text)
# - 邮箱移除
text = re.sub(r"\S+@\S+", " ", text)

# 4️⃣ 统一标点
# - 把常见中文标点映射为英文标点或空格(视需求而定,这里统一为空格)
punct_map = {
",": " ", "。": " ", "、": " ", ";": " ", "!": " ", "?": " ",
":": " ", "(": " ", ")": " ", "【": " ", "】": " ", "《": " ",
"》": " ", "“": " ", "”": " ", "‘": " ", "’": " ", "——": " ",
"……": " ", "·": " ", "—": " ", "~": " ", "『": " ", "』": " ",
}
for cn, en in punct_map.items():
text = text.replace(cn, en)

# 5️⃣ 过滤非文字字符(保留汉字、英文字母、数字、空格)
# Unicode 区间:汉字(\u4e00-\u9fff),基本拉丁字母、数字
text = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9\s]", " ", text)

# 6️⃣ 合并连续空格、换行、制表符为单个空格
text = re.sub(r"\s+", " ", text)

# 7️⃣ 去首尾空格
return text.strip()


# ---------- 示例 ----------
if __name__ == "__main__":
raw = """
<p>大家好!我今天想分享一些关于AI的想法。</p>
访问地址:https://example.com/ai?ref=blog
另外,以下是全角字符:hello,世界! 😊
还有一些英文标点: Hello, WORLD! \t\n
"""
print("原始文本:")
print(raw)

cleaned = normalize_text(raw)

print("\n规范化后:")
print(cleaned)

词元化

文本完成规范化后,需要将文本拆分成能够向量化的单元(token),这一步也叫词元化。词元化一般有以下 3 种方式:

**字符级:**每个字符都是一个词元,词表极小(中文仅几千个汉字),序列很长,单字符语义弱。

**词级:**对于英文,词元是以空格(或标点)分隔的子字符串;对于中文,则需要基于词典实现。词汇量大,词表会难以管理,且模型无法处理生僻词(OOV)。典型实现有 jieba 等。

**子词级:**将常见的词保留为完整的词元,同时将不常见的词拆分成多个有意义的子词片段。这样既控制了词表的大小,又能让模型通过组合子词来理解和生成新词。主流的实现有 BPE 算法等。

BPE 算法

BPE 的核心思想非常简单:反复把语料中出现频率最高的相邻符号对合并成一个新符号,直到词表达到预设大小。以下是采用 BPE 算法进行分词的简单示例。

语料:

  1. 我爱AI

  2. 我爱深度学习

步骤 当前字符序列(每个字符用空格分隔) 统计相邻字符对 最高频对 → 合并 合并后序列
初始词表 {我, 爱, A, I, 机, 器, 学, 习} — — —
统计相邻对 计数 (我, 爱)=2、(爱, A)=1、(A, I)=1、(机, 器)=1、(学, 习)=1 … (我, 爱) (2 次) 合并为 我爱 我爱 A I
我爱 机 器 学 习
合并最高频对 计数 (我爱, A)=1、(A, I)=1、(机, 器)=1、(学, 习)=1 … 任选(这里选 (A, I)) 合并为 AI 我爱 AI
我爱 机 器 学 习
继续统计、合并 计数 (我爱, AI)=1、(机, 器)=1、(学, 习)=1 … 任选(这里选 (机, 器)) 合并为 深度 我爱 AI
我爱 深度 学 习
最终结果 计数 (学, 习)=1 → 合并为 学习 合并后得到最终子词序列 我爱 AI
我爱 深度 学习
{我爱, AI, 深度, 学习}

最终子词词表(仅列出现的子词)

{我爱, AI, 深度, 学习}

建立词汇索引

得到词元后,需要将每个词元分配一个唯一整数 ID。词表在训练开始前就固定,之后不再改变。

如上面得到子词词表后,得到的词汇表如下:

1
{"我爱": 0, "AI": 1, "深度": 2, "学习": 3}

得到词汇表后,下一步我们要思考的是,如何表示一个句子。

编码

Multi-hot

表示一个句子最简单的做法是把一段文本看成一个词的集合,完全忽略顺序,这就是词袋模型。它用一个长度为 V(词表大小)的向量表示,每个维度对应词表中的一个词,出现了就标 1,没出现就标 0。

1
2
3
4
5
文本: "我爱深度学习"
词表: {"我爱": 0, "AI": 1, "深度": 2, "学习": 3}

Multi-hot 向量:
[1, 0, 1, 1]

但这种方式有很大的问题,它舍弃了词序,对同一个句子,把词序换一下,往往表示不同的含义。

比如,对 Multi-hot 来说,下面两句话是完全一样的:

1
2
"电影 不 好" → {电影, 不, 好}  
"电影 好 不" → {电影, 好, 不}

但实际上,这两个句子有着完全不同的含义。

One-hot

既然词袋丢了顺序,那我们把文本保持为一个有序的词元序列,每个词元用 One-hot 向量表示。

给定词表大小 V,token id 为 i 的词被编码为长度为 V 的向量,第 i 位为 1,其余为 0:

1
2
3
4
5
6
7
8
9
10
11
12
文本: "我爱深度学习"
词表: {"我爱": 0, "AI": 1, "深度": 2, "学习": 3}

整数序列:
[0, 2, 3]

One-hot 序列:
[
[1, 0, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
]

这里可能有人会有疑问,为什么不直接使用整数序列?直接输入整数序列会引入错误的数值假设(而实际上我们的词汇表索引大小可以认为是随机的,不含任何的意义),转成向量后可以有更大的表示空间。

经过 One-hot 编码后,词序保留了,但是问题来了,一条长度 20、词表 20000 的文本,经过 One-hot 编码后变成 (20, 20000) 的矩阵—— 包含 400 万个浮点数,其中 99.997% 是 0。

  • 内存浪费巨大,效率极低;
  • 任意两个词元的向量是相互独立的,不存在任何语义相似性,但在实际情况中,两个词向量之间的几何关系应该反映这两个单词之间的语义关系——比如,“好”和“棒”这两个词的“距离”应该相近。

我们需要更加聪明、更加高效的表示。

词嵌入

词嵌入是 One-hot 的高效实现:One-hot 编码得到的向量是二进制的、稀疏的(大部分元素是0)​、高维的(维度大小等于词表中的单词个数),是硬编码的​;而词嵌入是低维的浮点向量(密集向量,与稀疏向量相对),另外最重要的一点是,它是和模型一起训练、并且从数据中学习得到的。

如何得到词嵌入呢?有以下两种方法可以得到词嵌入。

  • 在完成主任务(比如文档分类或情感预测)的同时学习词嵌入。在这种情况下,神经网络中会有一个词嵌入层,嵌入层本质上就是一个查表操作,其参数在初始化时随机,在训练中更新,它的学习方式与学习神经网络权重相同,在学习过程中,语义相近的词元会逐渐靠近,语义相反的会远离。

  • 加载别人在大规模语料上预训练好的词向量。这种词嵌入通常是利用词频统计计算得到的,具有通用特征,如Word2Vec、GloVe等。

以下是训练一个词嵌入层的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
inputs  = keras.Input(shape=(None,), dtype="int64")     # 整数序列
x = layers.Embedding(
input_dim=20000, # 词表大小
output_dim=256, # 嵌入维度
)(inputs)
# x 的形状:(batch_size, seq_len, 256)
# 按实际任务接不同的神经网络层
x = layers.Bidirectional(layers.LSTM(32))(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)

model = keras.Model(inputs, outputs)
model.compile(optimizer="rmsprop", loss="binary_crossentropy", metrics=["accuracy"])

小结

本文介绍了深度学习中文本数据的向量化流程与主要编码方式。

文本向量化分为三个步骤:

  1. 文本规范化:统一文本格式(如转小写、全角转半角、去除噪声),减少无意义的编码差异,提升模型泛化能力。

  2. 词元化:将文本拆分为可向量化的最小单元。常见粒度有字符级、词级和子词级,其中子词级(如 BPE 算法)兼顾了词表大小与生僻词处理能力,是当前主流方案。

  3. 建立词汇索引:为每个词元分配唯一的整数 ID,构建固定词表。

编码方式的演进体现了对语义与效率的不断权衡:

  • Multi-hot(词袋):实现简单,但完全丢失词序,同义不同序的句子无法区分。
  • One-hot:保留了词序,但向量极度稀疏、维度极高,且无法表达词与词之间的语义相似性。
  • 词嵌入:用低维稠密向量表示词元,可与模型联合训练或使用预训练向量,不仅大幅降低内存开销,还能在几何空间中自然地编码词语间的语义关系,是目前最实用的文本表示方式。