【实战】自然语言处理--长文本分类(1)DPCNN算法
1.数据集
1. 来源与简介
- 名称:THUCNews
- 发布机构:清华大学自然语言处理与社会人文计算实验室(THUNLP)
- 规模:约 740 万篇中文新闻文本(完整版),本次使用子集共计 65 000 条样本
- 任务类型:多分类文本分类,针对长文本新闻内容进行主题判别
2. 本次使用的子集
| 文件名 | 样本数 | 说明 |
|---|---|---|
cnews.train.txt | 50 000 | 训练集 |
cnews.val.txt | 5 000 | 验证集 |
cnews.test.txt | 10 000 | 测试集 |
每行格式通常为:
<类别标签>\t<新闻正文文本>
3. 类别分布
本次精选了其中 10 个主题类别:
- 体育
- 财经
- 房产
- 家居
- 教育
- 科技
- 时尚
- 时政
- 游戏
- 娱乐
各类别样本数大致均衡,均在 5 000~7 000 条左右,可有效避免类别极度倾斜。
4. 文本特点
- 平均长度:每篇新闻正文常在 500~2 000 字之间,属于中长文本范畴。
- 内容风格:覆盖新闻报道、评论、特写、资讯等多种写作风格。
- 语言特点:专业术语、专有名词较多,需做好词表扩充或使用预训练模型词表。
2.DPCNN算法

1. 算法定义与背景
Deep Pyramid Convolutional Neural Network(DPCNN)是一种针对长文本分类任务设计的深度卷积神经网络,首次发表于 2016 年。它在传统卷积神经网络(CNN)基础上引入金字塔式下采样和残差连接,旨在以更少的参数和计算开销,高效捕获长文本的全局与局部特征。
2. 核心原理
2.1 区域嵌入(Region Embedding)
代码位置:模型初始化中
self.region_conv = nn.Conv1d(...)
# 在 DPCNN.__init__ 中
self.region_conv = nn.Conv1d(in_channels=self.embedding_dim,out_channels=self.num_filters,kernel_size=3,padding=1
)
- 作用:将词向量在局部窗口内(3-gram)进行卷积运算,提取低层次 n-gram 特征。
- 输入/输出维度:
- 输入:
(batch_size, embedding_dim, seq_len) - 输出:
(batch_size, num_filters, seq_len)
- 输入:
2.2 卷积块与残差连接(ConvBlock + Residual)
代码位置:
for i in range(self.repeat_blocks):block = nn.Sequential(nn.ReLU(),nn.Conv1d(self.num_filters, self.num_filters, kernel_size=3, padding=1),nn.ReLU(),nn.Conv1d(self.num_filters, self.num_filters, kernel_size=3, padding=1),)self.conv_blocks.append(block)
-
每个 ConvBlock 包含两层带 ReLU 的一维卷积。
-
残差连接:
- 第一个块不下采样:
out = block(x); x = x + out - 后续块先池化再卷积:
x = self.pool(x) out = block(x) x = x + out
- 第一个块不下采样:
-
残差结构保证深层网络中梯度稳定传递,加速收敛。
2.3 金字塔下采样(Pyramid Pooling)
代码位置:
self.pool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1)
- 每隔一个 ConvBlock,通过池化将序列长度减半。金字塔式下采样使感受野逐层扩大,兼具全局信息。
- 下采样后的序列长度
L' = ceil((L + 2*pad - kernel) / stride + 1) ≈ L/2。
3. 模型结构流程(结合 forward 代码)
def forward(self, x):# 1) Embedding: [B, L] -> [B, L, E]x = self.embedding(x)# 2) Dropout + 转置: [B, L, E] -> [B, E, L]x = self.embed_dropout(x).transpose(1, 2)# 3) 区域卷积: [B, E, L] -> [B, F, L]x = self.region_conv(x)# 4) 首个残差块x = self.embed_dropout(x)for idx, block in enumerate(self.conv_blocks):if idx == 0:out = block(x)x = x + outelse:# 5) 下采样 + 残差x = self.pool(x)out = block(x)x = x + out# 6) 全连接输出logits = self.fc(x)return logits
-
Embedding → Dropout:对输入词 ID 序列进行向量化并随机失活。
-
区域卷积:提取初级局部特征。
-
卷积+残差:
- 第一个
ConvBlock:保持序列长度。 - 后续块:池化后卷积,序列长度逐步缩减。
- 第一个
-
Flatten → FC:将最终特征图展平,输出
num_classes类别得分。
4. 代码流程详解
4.1 数据预处理与词表
# 清洗与分词
def clear_text(text):p = re.compile(r"[^一-龥0-9a-zA-Z\-…]")return p.sub('', text)def tokenize(text):text = clear_text(text)segs = jieba.lcut(text)return [w for w in segs if w not in STOPWORDS_SET]
- 正则只保留中英文、数字和常用标点。
jieba.lcut精准分词,去掉停用词。
# 词表构建或加载
def load_or_build_vocab(texts, force_rebuild=False):if os.path.exists(counter_path) and not force_rebuild:vocab = pickle.load(open(counter_path, 'rb'))else:vocab = build_vocab_from_iterator(map(tokenize, texts),max_tokens=total_words,specials=['<unk>','<pad>'])with open(counter_path, 'wb') as f:pickle.dump(vocab, f)vocab.set_default_index(vocab['<unk>'])return vocab
- 保存词表映射
word->index,并添加<unk>、<pad>。
4.2 数据加载与迭代
def load_data(path, train=False, vocab=None):texts, labels = read_data(path)if train:vocab = load_or_build_vocab(texts, force_rebuild=True)else:if vocab is None:vocab = pickle.load(open(counter_path, 'rb'))dataset = TextDataset(texts, labels, vocab, doc_maxlen)loader = DataLoader(dataset, batch_size=batch_size,shuffle=train, collate_fn=collate_fn)return loader, vocab
- 训练阶段重建词表;验证阶段仅加载或复用。
TextDataset中将文本切分、映射为固定长度 ID 序列。
4.3 训练与验证
def train_step(model, batch, optimizer):model.train()x, y = batchoptimizer.zero_grad()logits = model(x.to(device))loss = loss_func(logits, y.to(device))loss.backward()optimizer.step()pred = logits.argmax(dim=1).cpu().numpy()acc = accuracy_score(y.numpy(), pred)return loss.item(), acc
- 反向传播:计算梯度并更新参数。
- 指标:使用
accuracy_score评估批准确率。
@torch.no_grad()
def validate_step(model, batch):model.eval()x, y = batchlogits = model(x.to(device))loss = loss_func(logits, y.to(device))pred = logits.argmax(dim=1).cpu().numpy()acc = accuracy_score(y.numpy(), pred)return loss.item(), acc
- 在验证集上关闭梯度计算,加快速度并节省显存。
4.4 完整训练循环
for epoch in range(1, EPOCHS+1):# 训练for batch in train_loader:train_loss, train_acc = train_step(model, batch, optimizer)# 验证for batch in val_loader:val_loss, val_acc = validate_step(model, batch)# 日志记录 & 模型保存if val_acc > best_acc:torch.save(...)
- 每轮完成后打印损失/准确率,保存最佳模型。
5. 优缺点与扩展
优点
- 高效:金字塔下采样显著减少序列长度,降低计算开销。
- 易训练:残差连接缓解梯度消失。
- 参数量少:相比 RNN/LSTM 速度更快。
缺点
- 信息丢失:下采样会丢弃部分细节。
- 感受野固定:卷积核及层数需手工调优。
可扩展方向
- 多通道卷积:引入不同窗口大小并行卷积。
- 注意力机制:在下采样后加入自注意力,补充全局依赖。
- 层次化融合:结合 HAN、Transformer 架构,提高长依赖捕获能力。
3.补充问题
DPCNN的超参数
-
total_words = 20000
只保留训练语料中出现频率最高的 20,000 个词,其余词都映射为<unk>。- 作用:控制词表大小,减少稀有词带来的噪声和计算开销。
- 对长文本的影响:即使文本很长,也只会按最大词表截断——所有低频词统一处理成
<unk>,保证序列长度和词表维度都在可控范围内。
-
doc_maxlen = 500
每条文本被截断或填充到 500 个词(token)。- 截断:若文本长度 > 500,则只保留前 500 个 token,丢弃后面的部分。
- 填充:若文本长度 < 500,则在尾部补
<pad>直至长度为 500。 - 对长文本的影响:通过固定长度让所有输入张量尺寸统一。500 足以覆盖大多数新闻文章主体,同时截掉过长尾部,兼顾效率与完整性。
-
net_depth = 20
网络的总层数(区域嵌入层 + 若干卷积块 ×2 + 池化层),决定模型金字塔的高度。- 每两个卷积块后,下采样一次;20 层可以支持约 9~10 次下采样(实际到序列长度变为 1 时停止)。
- 对长文本的影响:更多深层意味着能对序列进行更多次的半速下采样,把原来 500 长度的序列,逐步缩减到几十、几级、直至 1,从而在最顶层获得整个文本的全局表征。
-
batch_size = 1024
每批在显存中同时处理 1024 条文本。- 对长文本的影响:虽然每条是 500 长度的张量,但大 batch size 能更高效利用 GPU 并行计算;如果显存不足,可调小。
-
其他超参数
embedding_dim = 200:词向量维度;影响每个词的表达能力。LR = 5e-4&EPOCHS = 30:学习率和训练轮数,影响收敛速度与最终效果。
DPCNN如何处理长文本
-
固定长度截断/填充:先把所有文本统一到
doc_maxlen = 500,保证输入张量尺寸一致。 -
区域嵌入(3-gram 卷积):在长度为 500 之上先做一次 1D 卷积,提取局部 n-gram 特征。
-
金字塔式下采样:
- 每经过两个卷积块,就通过
MaxPool1d(kernel=3, stride=2, padding=1)将序列长度减半。 - 层层下采样后,从 500 → ~250 → ~125 → … → 1(或很小),最后得到一个定长的特征图,融合了全局信息。
- 每经过两个卷积块,就通过
-
残差连接:每个卷积块前后的输入相加,保证即便文本很长,梯度也能顺畅向底层传递,不会在深层网络中消失。
这种“先定长截断 + 多次半速下采样 + 残差加速”的策略,使 DPCNN 能高效地处理和表征长文本,并在顶层快速聚合全局语义。
DPCNN就像是嵌套多层的漏斗
这个过程就像一个漏斗:
- 顶部宽大(原始文本长度长、信息多),
- 每经过一层卷积+池化,就压缩一次长度、升华语义,最终汇聚到底部的“分类表示”。
| 层级 | 序列长度(示意) | 特征维度(num_filters) |
|---|---|---|
| 输入文本 | 500 | 200(embedding_dim) |
| conv1×1 | 500 | 250 |
| block1 | 500 | 250 |
| pool1 | 250 | 250 |
| block2 | 250 | 250 |
| pool2 | 125 | 250 |
| block3 | 125 | 250 |
| pool3 | 62 | 250 |
| … | … | … |
| blockN | 1 | 250 |
| FC输出 | - | 10(类别数) |
