# 10.8 文本情感分類:使用卷積神經網絡(textCNN)
在“卷積神經網絡”一章中我們探究了如何使用二維卷積神經網絡來處理二維圖像數據。在之前的語言模型和文本分類任務中,我們將文本數據看作是只有一個維度的時間序列,并很自然地使用循環神經網絡來表征這樣的數據。其實,我們也可以將文本當作一維圖像,從而可以用一維卷積神經網絡來捕捉臨近詞之間的關聯。本節將介紹將卷積神經網絡應用到文本分析的開創性工作之一:textCNN [1]。
首先導入實驗所需的包和模塊。
``` python
import os
import torch
from torch import nn
import torchtext.vocab as Vocab
import torch.utils.data as Data
import torch.nn.functional as F
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
DATA_ROOT = "/S1/CSCL/tangss/Datasets"
```
## 10.8.1 一維卷積層
在介紹模型前我們先來解釋一維卷積層的工作原理。與二維卷積層一樣,一維卷積層使用一維的互相關運算。在一維互相關運算中,卷積窗口從輸入數組的最左方開始,按從左往右的順序,依次在輸入數組上滑動。當卷積窗口滑動到某一位置時,窗口中的輸入子數組與核數組按元素相乘并求和,得到輸出數組中相應位置的元素。如圖10.4所示,輸入是一個寬為7的一維數組,核數組的寬為2。可以看到輸出的寬度為`$ 7-2+1=6 $` ,且第一個元素是由輸入的最左邊的寬為2的子數組與核數組按元素相乘后再相加得到的:`$ 0\times1+1\times2=2 $`。
:-: 
<div align=center>圖10.4 一維互相關運算</div>
下面我們將一維互相關運算實現在`corr1d`函數里。它接受輸入數組`X`和核數組`K`,并輸出數組`Y`。
``` python
def corr1d(X, K):
w = K.shape[0]
Y = torch.zeros((X.shape[0] - w + 1))
for i in range(Y.shape[0]):
Y[i] = (X[i: i + w] * K).sum()
return Y
```
讓我們復現圖10.4中一維互相關運算的結果。
``` python
X, K = torch.tensor([0, 1, 2, 3, 4, 5, 6]), torch.tensor([1, 2])
corr1d(X, K)
```
輸出:
```
tensor([ 2., 5., 8., 11., 14., 17.])
```
多輸入通道的一維互相關運算也與多輸入通道的二維互相關運算類似:在每個通道上,將核與相應的輸入做一維互相關運算,并將通道之間的結果相加得到輸出結果。圖10.5展示了含3個輸入通道的一維互相關運算,其中陰影部分為第一個輸出元素及其計算所使用的輸入和核數組元素:`$ 0\times1+1\times2+1\times3+2\times4+2\times(-1)+3\times(-3)=2 $`。

<div align=center>圖10.5 含3個輸入通道的一維互相關運算</div>
讓我們復現圖10.5中多輸入通道的一維互相關運算的結果。
``` python
def corr1d_multi_in(X, K):
# 首先沿著X和K的第0維(通道維)遍歷并計算一維互相關結果。然后將所有結果堆疊起來沿第0維累加
return torch.stack([corr1d(x, k) for x, k in zip(X, K)]).sum(dim=0)
X = torch.tensor([[0, 1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6, 7],
[2, 3, 4, 5, 6, 7, 8]])
K = torch.tensor([[1, 2], [3, 4], [-1, -3]])
corr1d_multi_in(X, K)
```
輸出:
```
tensor([ 2., 8., 14., 20., 26., 32.])
```
由二維互相關運算的定義可知,多輸入通道的一維互相關運算可以看作單輸入通道的二維互相關運算。如圖10.6所示,我們也可以將圖10.5中多輸入通道的一維互相關運算以等價的單輸入通道的二維互相關運算呈現。這里核的高等于輸入的高。圖10.6中的陰影部分為第一個輸出元素及其計算所使用的輸入和核數組元素:`$ 2\times(-1)+3\times(-3)+1\times3+2\times4+0\times1+1\times2=2 $`。
:-: 
<div align=center>圖10.6 單輸入通道的二維互相關運算</div>
圖10.4和圖10.5中的輸出都只有一個通道。我們在5.3節(多輸入通道和多輸出通道)一節中介紹了如何在二維卷積層中指定多個輸出通道。類似地,我們也可以在一維卷積層指定多個輸出通道,從而拓展卷積層中的模型參數。
## 10.8.2 時序最大池化層
類似地,我們有一維池化層。textCNN中使用的時序最大池化(max-over-time pooling)層實際上對應一維全局最大池化層:假設輸入包含多個通道,各通道由不同時間步上的數值組成,各通道的輸出即該通道所有時間步中最大的數值。因此,時序最大池化層的輸入在各個通道上的時間步數可以不同。
為提升計算性能,我們常常將不同長度的時序樣本組成一個小批量,并通過在較短序列后附加特殊字符(如0)令批量中各時序樣本長度相同。這些人為添加的特殊字符當然是無意義的。由于時序最大池化的主要目的是抓取時序中最重要的特征,它通常能使模型不受人為添加字符的影響。
由于PyTorch沒有自帶全局的最大池化層,所以類似5.8節我們可以通過普通的池化來實現全局池化。
``` python
class GlobalMaxPool1d(nn.Module):
def __init__(self):
super(GlobalMaxPool1d, self).__init__()
def forward(self, x):
# x shape: (batch_size, channel, seq_len)
# return shape: (batch_size, channel, 1)
return F.max_pool1d(x, kernel_size=x.shape[2])
```
## 10.8.3 讀取和預處理IMDb數據集
我們依然使用和上一節中相同的IMDb數據集做情感分析。以下讀取和預處理數據集的步驟與上一節中的相同。
``` python
batch_size = 64
train_data = d2l.read_imdb('train', data_root=os.path.join(DATA_ROOT, "aclImdb"))
test_data = d2l.read_imdb('test', data_root=os.path.join(DATA_ROOT, "aclImdb"))
vocab = d2l.get_vocab_imdb(train_data)
train_set = Data.TensorDataset(*d2l.preprocess_imdb(train_data, vocab))
test_set = Data.TensorDataset(*d2l.preprocess_imdb(test_data, vocab))
train_iter = Data.DataLoader(train_set, batch_size, shuffle=True)
test_iter = Data.DataLoader(test_set, batch_size)
```
## 10.8.4 textCNN模型
textCNN模型主要使用了一維卷積層和時序最大池化層。假設輸入的文本序列由`$ n $`個詞組成,每個詞用`$ d $`維的詞向量表示。那么輸入樣本的寬為`$ n $`,高為1,輸入通道數為`$ d $`。textCNN的計算主要分為以下幾步。
1. 定義多個一維卷積核,并使用這些卷積核對輸入分別做卷積計算。寬度不同的卷積核可能會捕捉到不同個數的相鄰詞的相關性。
2. 對輸出的所有通道分別做時序最大池化,再將這些通道的池化輸出值連結為向量。
3. 通過全連接層將連結后的向量變換為有關各類別的輸出。這一步可以使用丟棄層應對過擬合。
圖10.7用一個例子解釋了textCNN的設計。這里的輸入是一個有11個詞的句子,每個詞用6維詞向量表示。因此輸入序列的寬為11,輸入通道數為6。給定2個一維卷積核,核寬分別為2和4,輸出通道數分別設為4和5。因此,一維卷積計算后,4個輸出通道的寬為`$ 11-2+1=10 $`,而其他5個通道的寬為`$ 11-4+1=8 $`。盡管每個通道的寬不同,我們依然可以對各個通道做時序最大池化,并將9個通道的池化輸出連結成一個9維向量。最終,使用全連接將9維向量變換為2維輸出,即正面情感和負面情感的預測。
:-: 
<div align=center>圖10.7 textCNN的設計</div>
下面我們來實現textCNN模型。與上一節相比,除了用一維卷積層替換循環神經網絡外,這里我們還使用了兩個嵌入層,一個的權重固定,另一個則參與訓練。
``` python
class TextCNN(nn.Module):
def __init__(self, vocab, embed_size, kernel_sizes, num_channels):
super(TextCNN, self).__init__()
self.embedding = nn.Embedding(len(vocab), embed_size)
# 不參與訓練的嵌入層
self.constant_embedding = nn.Embedding(len(vocab), embed_size)
self.dropout = nn.Dropout(0.5)
self.decoder = nn.Linear(sum(num_channels), 2)
# 時序最大池化層沒有權重,所以可以共用一個實例
self.pool = GlobalMaxPool1d()
self.convs = nn.ModuleList() # 創建多個一維卷積層
for c, k in zip(num_channels, kernel_sizes):
self.convs.append(nn.Conv1d(in_channels = 2*embed_size,
out_channels = c,
kernel_size = k))
def forward(self, inputs):
# 將兩個形狀是(批量大小, 詞數, 詞向量維度)的嵌入層的輸出按詞向量連結
embeddings = torch.cat((
self.embedding(inputs),
self.constant_embedding(inputs)), dim=2) # (batch, seq_len, 2*embed_size)
# 根據Conv1D要求的輸入格式,將詞向量維,即一維卷積層的通道維(即詞向量那一維),變換到前一維
embeddings = embeddings.permute(0, 2, 1)
# 對于每個一維卷積層,在時序最大池化后會得到一個形狀為(批量大小, 通道大小, 1)的
# Tensor。使用flatten函數去掉最后一維,然后在通道維上連結
encoding = torch.cat([self.pool(F.relu(conv(embeddings))).squeeze(-1) for conv in self.convs], dim=1)
# 應用丟棄法后使用全連接層得到輸出
outputs = self.decoder(self.dropout(encoding))
return outputs
```
創建一個`TextCNN`實例。它有3個卷積層,它們的核寬分別為3、4和5,輸出通道數均為100。
``` python
embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]
net = TextCNN(vocab, embed_size, kernel_sizes, nums_channels)
```
### 10.8.4.1 加載預訓練的詞向量
同上一節一樣,加載預訓練的100維GloVe詞向量,并分別初始化嵌入層`embedding`和`constant_embedding`,前者參與訓練,而后者權重固定。
``` python
glove_vocab = Vocab.GloVe(name='6B', dim=100,
cache=os.path.join(DATA_ROOT, "glove"))
net.embedding.weight.data.copy_(
d2l.load_pretrained_embedding(vocab.itos, glove_vocab))
net.constant_embedding.weight.data.copy_(
d2l.load_pretrained_embedding(vocab.itos, glove_vocab))
net.constant_embedding.weight.requires_grad = False
```
### 10.8.4.2 訓練并評價模型
現在就可以訓練模型了。
``` python
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, net.parameters()), lr=lr)
loss = nn.CrossEntropyLoss()
d2l.train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)
```
輸出:
```
training on cuda
epoch 1, loss 0.4858, train acc 0.758, test acc 0.832, time 42.8 sec
epoch 2, loss 0.1598, train acc 0.863, test acc 0.868, time 42.3 sec
epoch 3, loss 0.0694, train acc 0.917, test acc 0.876, time 42.3 sec
epoch 4, loss 0.0301, train acc 0.956, test acc 0.871, time 42.4 sec
epoch 5, loss 0.0131, train acc 0.979, test acc 0.865, time 42.3 sec
```
下面,我們使用訓練好的模型對兩個簡單句子的情感進行分類。
``` python
d2l.predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'great']) # positive
```
``` python
d2l.predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'bad']) # negative
```
## 小結
* 可以使用一維卷積來表征時序數據。
* 多輸入通道的一維互相關運算可以看作單輸入通道的二維互相關運算。
* 時序最大池化層的輸入在各個通道上的時間步數可以不同。
* textCNN主要使用了一維卷積層和時序最大池化層。
## 參考文獻
[1] Kim, Y. (2014). Convolutional neural networks for sentence classification. arXiv preprint arXiv:1408.5882.
-----------
> 注:本節除代碼外與原書基本相同,[原書傳送門](https://zh.d2l.ai/chapter_natural-language-processing/sentiment-analysis-cnn.html)
- Home
- Introduce
- 1.深度學習簡介
- 深度學習簡介
- 2.預備知識
- 2.1環境配置
- 2.2數據操作
- 2.3自動求梯度
- 3.深度學習基礎
- 3.1 線性回歸
- 3.2 線性回歸的從零開始實現
- 3.3 線性回歸的簡潔實現
- 3.4 softmax回歸
- 3.5 圖像分類數據集(Fashion-MINST)
- 3.6 softmax回歸的從零開始實現
- 3.7 softmax回歸的簡潔實現
- 3.8 多層感知機
- 3.9 多層感知機的從零開始實現
- 3.10 多層感知機的簡潔實現
- 3.11 模型選擇、反向傳播和計算圖
- 3.12 權重衰減
- 3.13 丟棄法
- 3.14 正向傳播、反向傳播和計算圖
- 3.15 數值穩定性和模型初始化
- 3.16 實戰kaggle比賽:房價預測
- 4 深度學習計算
- 4.1 模型構造
- 4.2 模型參數的訪問、初始化和共享
- 4.3 模型參數的延后初始化
- 4.4 自定義層
- 4.5 讀取和存儲
- 4.6 GPU計算
- 5 卷積神經網絡
- 5.1 二維卷積層
- 5.2 填充和步幅
- 5.3 多輸入通道和多輸出通道
- 5.4 池化層
- 5.5 卷積神經網絡(LeNet)
- 5.6 深度卷積神經網絡(AlexNet)
- 5.7 使用重復元素的網絡(VGG)
- 5.8 網絡中的網絡(NiN)
- 5.9 含并行連結的網絡(GoogLeNet)
- 5.10 批量歸一化
- 5.11 殘差網絡(ResNet)
- 5.12 稠密連接網絡(DenseNet)
- 6 循環神經網絡
- 6.1 語言模型
- 6.2 循環神經網絡
- 6.3 語言模型數據集(周杰倫專輯歌詞)
- 6.4 循環神經網絡的從零開始實現
- 6.5 循環神經網絡的簡單實現
- 6.6 通過時間反向傳播
- 6.7 門控循環單元(GRU)
- 6.8 長短期記憶(LSTM)
- 6.9 深度循環神經網絡
- 6.10 雙向循環神經網絡
- 7 優化算法
- 7.1 優化與深度學習
- 7.2 梯度下降和隨機梯度下降
- 7.3 小批量隨機梯度下降
- 7.4 動量法
- 7.5 AdaGrad算法
- 7.6 RMSProp算法
- 7.7 AdaDelta
- 7.8 Adam算法
- 8 計算性能
- 8.1 命令式和符號式混合編程
- 8.2 異步計算
- 8.3 自動并行計算
- 8.4 多GPU計算
- 9 計算機視覺
- 9.1 圖像增廣
- 9.2 微調
- 9.3 目標檢測和邊界框
- 9.4 錨框
- 10 自然語言處理
- 10.1 詞嵌入(word2vec)
- 10.2 近似訓練
- 10.3 word2vec實現
- 10.4 子詞嵌入(fastText)
- 10.5 全局向量的詞嵌入(Glove)
- 10.6 求近義詞和類比詞
- 10.7 文本情感分類:使用循環神經網絡
- 10.8 文本情感分類:使用卷積網絡
- 10.9 編碼器--解碼器(seq2seq)
- 10.10 束搜索
- 10.11 注意力機制
- 10.12 機器翻譯