1.數據集
1. 來源與簡介
- 名稱:THUCNews
- 發佈機構:清華大學自然語言處理與社會人文計算實驗室(THUNLP)
- 規模:約 740 萬篇中文新聞文本(完整版),本次使用子集共計 65 000 條樣本
- 任務類型:多分類文本分類,針對長文本新聞內容進行主題判別
2. 本次使用的子集
|
文件名
|
樣本數
|
説明
|
|
|
50 000
|
訓練集
|
|
|
5 000
|
驗證集
|
|
|
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 + out
else:
# 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 = 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評估批准確率。
@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 完整訓練循環
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(類別數)
|