该部分内容通过代码注释的形式说明。
一、TextCNN 核心部分代码
import torch
import torch.nn as nn
class TextCNN(nn.Module):
def __init__(self, embedding_dim, n_filters, filter_sizes, output_dim,
dropout, pretrained_embeddings):
super().__init__()
# 参数 pretrained_embeddings 是预训练好的词向量
# pretrained_embeddings 词向量可以通过 https://www.jianshu.com/p/34c1e23ad4fd 中的方式获得
self.embedding = nn.Embedding.from_pretrained(
pretrained_embeddings, freeze=False)
# Conv1d:in_channels 指的是输入的词向量的维度
# Conv1d:out_channels 用来指定使用多少个卷积核
# Conv1d:kernel_size 指定卷积核的窗口大小,由于 Conv1d 是一维卷积,所以我们仅需指定卷积核的大小
# 即可,卷积核的另一个维度等于词向量的维度
# nn.Sequential 内部定义的是一组操作,组内的操作流程是用 n_filters 个窗口大小为 fs 的卷积核对相
# 应的输入进行特征提取,得到 n_filters 个特征向量。这 n_filters 个特征向量再通过
# ReLu 激活函数得到 n_filters 个被激活的特征向量。最后这些向量被最大池化。
# MaxPool1d:kernel_size=10-fs+1,其中“10”指的是 sequence_length,为了方便实验,这里为 sequence
# length 指定了一个较小的数字 10。fs 是卷积层中卷积核的窗口大小。实际上 10-fs+1
# 就是使用卷积核进行特征提取后每个特征向量的维度数目。于是 nn.MaxPool1d(kernel_size=10-fs+1)
# 的作用就是在每个特征向量的所有维度中找出数值最大的那个数字来代表当前特征向量。
# for 循环的理解:假设 filter_size 取值为 [2, 3, 4],则该 for 循环的作用是生成 3 个 nn.Sequential
# 组操作,第一组中卷积核窗口大小为 2,第二组中卷积核窗口大小为 3,第三组中卷积核窗口大小为 4。
# 这三个组之间的关系现在还没有确定,目前只是定义了这么三个组操作。
self.convs = nn.ModuleList([
nn.Sequential(nn.Conv1d(in_channels=embedding_dim,
out_channels=n_filters,
kernel_size=fs),
nn.ReLU(),
nn.MaxPool1d(kernel_size=10-fs+1))
for fs in filter_sizes
])
# 全连接层
# Linear:out_features=output_dim 这个值的设定很好理解,就是输出标签的数量
# Linear:in_features 的取值比较抽象,在不了解网络结构的情况下,理解这个取值还是比较绕的,
# 所以这里先对网络结构简单介绍一下:每个卷积核对原始的输入数据进行特征提取并池化后都
# 会得到一个数值,而上面卷积核的数量就是 len(filter_sizes)*n_filters ,即
# (Sequential 组数量)*(每个 Sequential 组中卷积核的数量),也就是说经过上面
# 所有卷积核的特征提取并池化后得到了 len(filter_sizes)*n_filters 个数值。
# 现在就容易理解 Linear:in_features 的取值了。
self.fc = nn.Linear(in_features=len(filter_sizes)*n_filters,
out_features=output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# text: [sent len, batch size]
# text 是由下标表示
text, _ = x
# text: [batch size, sent len]
text = text.permute(1, 0)
# embedded: [batch size, sent len, emb dim]
embedded = self.embedding(text)
# embedded = [batch size, emb dim, sent len]
embedded = embedded.permute(0, 2, 1)
out = [conv(embedded) for conv in self.convs]
print('>1: ', out)
print('>>>>>>>>>>>>>>>>>>>>>>')
out = torch.cat(out, dim=1)
print('>2: ', out)
print('>>>>>>>>>>>>>>>>>>>>>>')
out = out.view(-1, out.size(1))
print('>3: ', out)
out = self.dropout(out)
return self.fc(out)
至此,本文要说明的内容结束了。
二、完整 demo
import torch
import torch.nn as nn
from torchtext import data
from torchtext import vocab
from tqdm import tqdm
class TextCNN(nn.Module):
def __init__(self, embedding_dim, n_filters, filter_sizes, output_dim,
dropout, pretrained_embeddings):
super().__init__()
# 参数 pretrained_embeddings 是预训练好的词向量
# pretrained_embeddings 词向量可以通过 https://www.jianshu.com/p/34c1e23ad4fd 中的方式获得
self.embedding = nn.Embedding.from_pretrained(
pretrained_embeddings, freeze=False)
# self.convs = Conv1d(embedding_dim, n_filters, filter_sizes)
# Conv1d:in_channels 指的是输入的词向量的维度
# Conv1d:out_channels 用来指定使用多少个卷积核
# Conv1d:kernel_size 指定卷积核的窗口大小,由于 Conv1d 是一维卷积,所以我们仅需指定卷积核的大小
# 即可,卷积核的另一个维度等于词向量的维度
# nn.Sequential 内部定义的是一组操作,组内的操作流程是用 n_filters 个窗口大小为 fs 的卷积核对相
# 应的输入进行特征提取,得到 n_filters 个特征向量。这 n_filters 个特征向量再通过
# ReLu 激活函数得到 n_filters 个被激活的特征向量。最后这些向量被最大池化。
# MaxPool1d:kernel_size=10-fs+1,其中“10”指的是 sequence_length,为了方便实验,这里为 sequence
# length 指定了一个较小的数字 10。fs 是卷积层中卷积核的窗口大小。实际上 10-fs+1
# 就是使用卷积核进行特征提取后每个特征向量的维度数目。于是 nn.MaxPool1d(kernel_size=10-fs+1)
# 的作用就是在每个特征向量的所有维度中找出数值最大的那个数字来代表当前特征向量。
# for 循环的理解:假设 filter_size 取值为 [2, 3, 4],则该 for 循环的作用是生成 3 个 nn.Sequential
# 组操作,第一组中卷积核窗口大小为 2,第二组中卷积核窗口大小为 3,第三组中卷积核窗口大小为 4。
# 这三个组之间的关系现在还没有确定,目前只是定义了这么三个组操作。
self.convs = nn.ModuleList([
nn.Sequential(nn.Conv1d(in_channels=embedding_dim,
out_channels=n_filters,
kernel_size=fs),
nn.ReLU(),
nn.MaxPool1d(kernel_size=10-fs+1))
for fs in filter_sizes
])
# 全连接层
# Linear:out_features=output_dim 这个值的设定很好理解,就是输出标签的数量
# Linear:in_features 的取值比较抽象,在不了解网络结构的情况下,理解这个取值还是比较绕的,
# 所以这里先对网络结构简单介绍一下:每个卷积核对原始的输入数据进行特征提取并池化后都
# 会得到一个数值,而上面卷积核的数量就是 len(filter_sizes)*n_filters ,即
# (Sequential 组数量)*(每个 Sequential 组中卷积核的数量),也就是说经过上面
# 所有卷积核的特征提取并池化后得到了 len(filter_sizes)*n_filters 个数值。
# 现在就容易理解 Linear:in_features 的取值了。
self.fc = nn.Linear(in_features=len(filter_sizes)*n_filters,
out_features=output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# text: [sent len, batch size]
# text 是由下标表示
text, _ = x
# text: [batch size, sent len]
text = text.permute(1, 0)
# embedded: [batch size, sent len, emb dim]
embedded = self.embedding(text)
# embedded = [batch size, emb dim, sent len]
embedded = embedded.permute(0, 2, 1)
out = [conv(embedded) for conv in self.convs]
print('>1: ', out)
print('>>>>>>>>>>>>>>>>>>>>>>')
out = torch.cat(out, dim=1)
print('>2: ', out)
print('>>>>>>>>>>>>>>>>>>>>>>')
out = out.view(-1, out.size(1))
print('>3: ', out)
out = self.dropout(out)
return self.fc(out)
if __name__ == '__main__':
embedding_file = '/home/jason/Desktop/data/embeddings/glove/glove.6B.300d.txt'
path = '/home/jason/Desktop/data/SST-2/'
cache_dir = '.cache/'
batch_size = 6
vectors = vocab.Vectors(embedding_file, cache_dir)
text_field = data.Field(tokenize='spacy',
lower=True,
include_lengths=True,
fix_length=10)
label_field = data.LabelField(dtype=torch.long)
train, dev, test = data.TabularDataset.splits(path=path,
train='train.tsv',
validation='dev.tsv',
test='test.tsv',
format='tsv',
skip_header=True,
fields=[('text', text_field), ('label', label_field)])
text_field.build_vocab(train,
dev,
test,
max_size=25000,
vectors=vectors,
unk_init=torch.Tensor.normal_)
label_field.build_vocab(train, dev, test)
pretrained_embeddings = text_field.vocab.vectors
labels = label_field.vocab.vectors
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
train_iter, dev_iter, test_iter = data.BucketIterator.splits((train, dev, test),
batch_sizes=(batch_size, len(dev), len(test)),
sort_key=lambda x: len(x.text),
sort_within_batch=True,
repeat=False,
shuffle=True,
device=device
)
model = TextCNN(300, 2, [2, 3, 4], 2, 0.8, pretrained_embeddings)
for step, batch in enumerate(tqdm(train_iter, desc="Iteration")):
logits = model(batch.text)
break
为了便于观察矩阵的变化情况,输出了一些中间结果:
Iteration: 0%| | 0/11225 [00:00, ?it/s]>1: [tensor([[[0.1120],
[0.2917]],
[[0.2548],
[0.4744]],
[[0.0000],
[0.3546]],
[[0.1336],
[0.4589]],
[[0.3120],
[0.2878]],
[[0.0725],
[0.2698]]], grad_fn=), tensor([[[0.2469],
[0.4527]],
[[0.1795],
[0.3658]],
[[0.2063],
[0.2909]],
[[0.1798],
[0.2380]],
[[0.1037],
[0.2685]],
[[0.2906],
[0.3949]]], grad_fn=), tensor([[[0.1052],
[0.0000]],
[[0.1764],
[0.3627]],
[[0.0577],
[0.1295]],
[[0.1903],
[0.0831]],
[[0.0393],
[0.0104]],
[[0.0755],
[0.0793]]], grad_fn=)]
>>>>>>>>>>>>>>>>>>>>>>
>2: tensor([[[0.1120],
[0.2917],
[0.2469],
[0.4527],
[0.1052],
[0.0000]],
[[0.2548],
[0.4744],
[0.1795],
[0.3658],
[0.1764],
[0.3627]],
[[0.0000],
[0.3546],
[0.2063],
[0.2909],
[0.0577],
[0.1295]],
[[0.1336],
[0.4589],
[0.1798],
[0.2380],
[0.1903],
[0.0831]],
[[0.3120],
[0.2878],
[0.1037],
[0.2685],
[0.0393],
[0.0104]],
[[0.0725],
[0.2698],
[0.2906],
[0.3949],
[0.0755],
[0.0793]]], grad_fn=)
>>>>>>>>>>>>>>>>>>>>>>
>3: tensor([[0.1120, 0.2917, 0.2469, 0.4527, 0.1052, 0.0000],
[0.2548, 0.4744, 0.1795, 0.3658, 0.1764, 0.3627],
[0.0000, 0.3546, 0.2063, 0.2909, 0.0577, 0.1295],
[0.1336, 0.4589, 0.1798, 0.2380, 0.1903, 0.0831],
[0.3120, 0.2878, 0.1037, 0.2685, 0.0393, 0.0104],
[0.0725, 0.2698, 0.2906, 0.3949, 0.0755, 0.0793]],
grad_fn=)
Iteration: 0%| | 0/11225 [00:00, ?it/s]