文本预处理

文本可以认为是一个时间序列,对于文本中的每个字或者词都可以认为是一个向量,这些向量之间有先后顺序关系。

对于一个文本序列,我们可以进行字符级语言建模或者词级语言建模:字级建模就是将每个字当成一个时间步的数据,比如“我要学习英语”,字符级语言建模会将其拆分为“我”、“要”、“学”、“习”、“英”、“语”;词级语言建模会将文本拆分成一个一个的词语,以此为最小单位作为每个时间步的数据,比如“我要学英语”,词级建模可能会将其拆分为“我”、“要”、“学习”、“英语”。观察这两个例子,如果是字符级建模,“英”其实可以与很多不同的字组合形成不同的意思,比如“英雄”、“英国”、“英语”、“英气”、“英年”等等,这给模型带来了更大的难度去理解不同时间步之间的意思;词级建模以单词为基本单位,天然携带完整的语义信息。因此,词级建模的效果要好于字符级建模。

词级建模的算力要求相对于字符级更低,因为同一个句子如果拆分成字符级会有更多的时间步。

拆分词元

对于中文而言,首先我们要将文本拆分成token(词元),即一个个的时间步的数据,如果使用词级建模,可以使用jieba库将文本拆分成词语:

1
2
3
4
import jieba

def tokenized(text):
return [word for word in jieba.cut(text) if word.strip()]

jieba.cut()可以将text文本拆分成词语,返回的是一个generator(生成器),可以用循环或list()等将其按顺序读取,strip()方法是去除字符串前面和后面的空字符(空格、制表符等)。

构建词汇表

将文本拆分成词元后,我们需要将这些词元进行统计,去除重复的词元,统计各个词元出现的次数,并给每个词元一个数字编号,这样我们就可以得到一个词元和其对应的数字编号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from collections import Counter

def build_vocab(tokenized_text, vocab_limit):
# 统计每个词元出现的次数,返回`词元: 出现次数`格式的字典
word_counts = Counter(tokenized_text)

# 按照最大词汇表限制vocab_limit构建词汇表,并保留一个位置给`<UNK>`,我们更应该保留出现次数多的词汇,因此按出现次数由多到少排序
vocab = sorted(word_counts, key=word_counts.get, reverse=True)[: vocab_limit - 1]
vocab = ['<PAD>', '<UNK>'] + vocab

word_to_idx = {word: i for i, word in enumerate(vocab)}

return word_to_idx, vocab

词汇表构建示例:

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
import jieba
from collections import Counter

def tokenized(text):
return [word for word in jieba.cut(text) if word.strip()]

def build_vocab(tokenized_text, vocab_limit):
word_counts = Counter(tokenized_text)
vocab = sorted(word_counts, key=word_counts.get, reverse=True)[: vocab_limit - 1]
vocab = ['<PAD>', '<UNK>'] + vocab
word_to_idx = {word: i for i, word in enumerate(vocab)}
return word_to_idx, vocab

file_path = './data/data.txt'
vocab_limit = 10000

with open(file_path, 'r') as f:
text = f.read()
tokenized_text = tokenized(text)

# 构建词汇表
word_to_idx, vocab = build_vocab(tokenized_text, vocab_limit)
idx_to_word = {i: word for i, word in enumerate(vocab)}
vocab_size = len(vocab)
print(f'词汇表大小:{vocab_size}')

文本转数字序列

词汇表构建完成后,我们需要将原本的文本序列变成数字序列,也就是将每个词元变成对应的数字编号:

1
data = [word_to_idx[word] if word in word_to_idx else word_to_idx['<UNK>'] for word in tokenized_text]

词元向量化

在模型中,直接使用词元对应的数字编码来进行计算效果是不好的,整数编码仅将词映射为离散的整数值,但数值本身无实际意义,模型无法通过整数之间的差值或比例推导出词语的语义关联。

因此,我们需要将词元向量化处理,最容易想到的方法是One-Hot编码(独热编码),也就是将仅在向量的某个维度值为1,其余维度值为0,以此区分不同的词元。如果使用独热编码,有多少个词元,向量就有多少个维度,并且每个词元的向量均只有一个维度的值不为零,这样维度极高的稀疏向量会占用非常大的内存空间,计算效率也非常低,并且,独热编码不能区分两个词元之间是否具有一定的关联性(比如近义词等),因为对于两个词元,它们在欧式空间的距离均为\(\sqrt{2}\)

因此,我们需要将词元构建成一个低纬度稠密向量,常用的方法有TF-IDFWord2VecBERT动态向量等,此处我们使用最简单的nn.Embedding()方法,原理是使用标准正态分布(均值为0,方差为1)随机初始化的方式生成词元向量:

nn.Embedding()的参数:

  • num_embedding:定义嵌入字典的大小,即词表的总词元数。该参数通常等于词汇表大小(如 vocab_size
  • embedding_dim:定义每个词元嵌入向量的维度。维度越高,语义表达能力越强,但计算成本也更高。高维度适合复杂任务(如机器翻译),低维度适合轻量级任务(如文本分类)

可选参数:

  • padding_idx:默认None
  • max_norm:浮点数,默认None,对嵌入向量进行范数裁剪。若向量范数超过该值,则按 norm_type 缩放至该值,防止梯度爆炸
  • norm_type:浮点数,默认2.0,定义范数裁剪时使用的范数类型。例如,2.0 表示 L2 范数,1.0 表示 L1 范数
  • scale_grad_by_freq:布尔值,默认False,若设为 True,梯度会根据词元在批次中的出现频率进行缩放。高频词元的梯度会被缩小,低频词元梯度放大,用于平衡数据分布
  • sparse:布尔值,默认False,若设为 True,使用稀疏梯度更新以节省内存,但会牺牲计算速度。适用于词表极大(如百万级)的场景
  • _weight:张量,默认None,自定义嵌入矩阵的初始化权重。需满足形状 (num_embeddings, embedding_dim),常用于加载预训练词向量(如 Word2Vec、GloVe)

词元向量化可以放在神经网络的定义中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class WordRNN(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size, num_layers):
super().__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers

self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.RNN(embed_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, vocab_size)

def forward(self, x, hidden):
x = self.embedding(x)
out, hidden = self.rnn(x, hidden)
out = self.fc(out)
return out, hidden

def init_hidden(self, batch_size, device):
return torch.zeros(num_layers, batch_size, self.hidden_size).to(device)

这样,输入的x只需要是文本对应的数字编号即可


文本预处理
https://blog.shinebook.net/2025/03/30/人工智能/pytorch/文本预处理/
作者
X
发布于
2025年3月30日
许可协议