【实战】自然语言处理--长文本分类(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 个主题类别:

  1. 体育
  2. 财经
  3. 房产
  4. 家居
  5. 教育
  6. 科技
  7. 时尚
  8. 时政
  9. 游戏
  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(...)

python 复制代码
# 在 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)

代码位置

python 复制代码
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

    • 后续块先池化再卷积:

      python 复制代码
      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 代码)

python 复制代码
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 + out
        else:
            # 5) 下采样 + 残差
            x = self.pool(x)
            out = block(x)
            x = x + out

    # 6) 全连接输出
    logits = self.fc(x)
    return logits
  1. Embedding → Dropout:对输入词 ID 序列进行向量化并随机失活。

  2. 区域卷积:提取初级局部特征。

  3. 卷积+残差

    • 第一个 ConvBlock:保持序列长度。
    • 后续块:池化后卷积,序列长度逐步缩减。
  4. Flatten → FC :将最终特征图展平,输出 num_classes 类别得分。

4. 代码流程详解

4.1 数据预处理与词表

python 复制代码
# 清洗与分词
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]
  1. 正则只保留中英文、数字和常用标点。
  2. jieba.lcut 精准分词,去掉停用词。
python 复制代码
# 词表构建或加载
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 数据加载与迭代

python 复制代码
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 训练与验证

python 复制代码
def train_step(model, batch, optimizer):
    model.train()
    x, y = batch
    optimizer.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 评估批准确率。
python 复制代码
@torch.no_grad()
def validate_step(model, batch):
    model.eval()
    x, y = batch
    logits = 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 完整训练循环

python 复制代码
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的超参数

  1. total_words = 20000

    只保留训练语料中出现频率最高的 20,000 个词,其余词都映射为 <unk>

    • 作用:控制词表大小,减少稀有词带来的噪声和计算开销。
    • 对长文本的影响 :即使文本很长,也只会按最大词表截断------所有低频词统一处理成 <unk>,保证序列长度和词表维度都在可控范围内。
  2. doc_maxlen = 500

    每条文本被截断或填充到 500 个词(token)。

    • 截断:若文本长度 > 500,则只保留前 500 个 token,丢弃后面的部分。
    • 填充 :若文本长度 < 500,则在尾部补 <pad> 直至长度为 500。
    • 对长文本的影响:通过固定长度让所有输入张量尺寸统一。500 足以覆盖大多数新闻文章主体,同时截掉过长尾部,兼顾效率与完整性。
  3. net_depth = 20

    网络的总层数(区域嵌入层 + 若干卷积块 ×2 + 池化层),决定模型金字塔的高度。

    • 每两个卷积块后,下采样一次;20 层可以支持约 9~10 次下采样(实际到序列长度变为 1 时停止)。
    • 对长文本的影响:更多深层意味着能对序列进行更多次的半速下采样,把原来 500 长度的序列,逐步缩减到几十、几级、直至 1,从而在最顶层获得整个文本的全局表征。
  4. batch_size = 1024

    每批在显存中同时处理 1024 条文本。

    • 对长文本的影响:虽然每条是 500 长度的张量,但大 batch size 能更高效利用 GPU 并行计算;如果显存不足,可调小。
  5. 其他超参数

    • 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(类别数)
相关推荐
I'm a winner4 小时前
基于YOLO算法的医疗应用专题:第一章 计算机视觉与深度学习概述
算法·yolo·计算机视觉
vir024 小时前
P1928 外星密码(dfs)
java·数据结构·算法·深度优先·1024程序员节
喜欢吃燃面4 小时前
数据结构算法题:list
开发语言·c++·学习·算法·1024程序员节
寂静山林4 小时前
UVa 12991 Game Rooms
算法·1024程序员节
余俊晖5 小时前
RLVR训练多模态文档解析模型-olmOCR 2技术方案(模型、数据和代码均开源)
人工智能·算法·ocr·grpo
凉虾皮6 小时前
2024包河初中组
学习·算法·1024程序员节
m0_748233646 小时前
C++ 模板初阶:从函数重载到泛型编程的优雅过渡
java·c++·算法·1024程序员节
以己之6 小时前
11.盛最多水的容器
java·算法·双指针·1024程序员节
初级炼丹师(爱说实话版)7 小时前
算法面经常考题整理(3)大模型
算法