20天狂宴Pytorch-Day3

GitHub链接

文本数据建模.

配环境配了一上午, gensim 需要scipy<1.14.0,>=1.7.0, 没有对应的 wheel, pip 决定下载源码自己编译, 然后报了个网上搜不到的罕见错误, 升级了两遍我的 MinGW, 从网上下了个 patch.exe, 装了个 MSYS2, 试图装了 pkg-config 和 openblas, 还把之前卸掉的 visual studio 装了回来, 最后还是没搞好.

然后发现是 python 版本不兼容导致没有 wheel, 换到 python3.10 马上就好了. 吃大份去吧.

准备数据

imdb 数据集的目标是根据电影评论的文本内容预测评论的情感标签, 训练集有 20000 条电影评论文本, 测试集有 5000 条电影评论文本, 其中正面评论和负面评论各占一半.

这里使用 gensim 中的词典工具, 自定义 Dataset.

from gensim import corpora
import string

# 去掉标点符号, 把句子按空格分成单词
def textsplit(text):
translator = str.maketrans('', '', string.punctuation)
words = text.translate(translator).split(' ')
return words

# 构建词典
# gensim的Dictionary会统计文本中的所有单词, 给每个单词一个唯一编号
vocab = corpora.Dictionary((textsplit(text) for text in dftrain['text']))
vocab.filter_extremes(no_below=5,no_above=5000)
special_tokens = {'<pad>': 0, '<unk>': 1}
vocab.patch_with_special_tokens(special_tokens)
vocab_size = len(vocab.token2id)
print('vocab_size = ',vocab_size)

# 序列填充
# 深度学习中, 模型一般需要固定长度的输入, 因此需要用<pad>填充或截断
def pad(seq,max_length,pad_value=0):
n = len(seq)
result = seq+[pad_value]*max_length
return result[:max_length]


# 编码转换
def text_pipeline(text):
tokens = vocab.doc2idx(textsplit(text))
tokens = [x if x>0 else special_tokens['<unk>'] for x in tokens ]
result = pad(tokens,MAX_LEN,special_tokens['<pad>'])
return result

print(text_pipeline("this is an example!"))


# 构建数据管道
from torch.utils.data import Dataset,DataLoader

class ImdbDataset(Dataset):
def __init__(self,df):
self.df = df
def __len__(self):
return len(self.df)
def __getitem__(self,index):
text = self.df["text"].iloc[index]
label = torch.tensor([self.df["label"].iloc[index]]).float()
tokens = torch.tensor(text_pipeline(text)).int()
return tokens,label

ds_train = ImdbDataset(dftrain)
ds_val = ImdbDataset(dfval)

dl_train = DataLoader(ds_train,batch_size = 50,shuffle = True)
dl_val = DataLoader(ds_val,batch_size = 50,shuffle = False)

建立模型

Pytorch 通常有三种方式构建模型: 使用 nn.Sequential 按层顺序构建模型, 继承 nn.Module 基类构建自定义模型, 继承 nn.Module 基类构建模型并辅助应用模型容器进行封装, 此处选用继承 nn.Module 构建模型并辅助应用模型容器 (nn.Sequential, nn.ModuleList, nn.ModuleDict) 进行封装.

import torch
from torch import nn
torch.manual_seed(42)

class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()

# 嵌入层, 把句子转换为矩阵
# 设置padding_idx参数后将在训练过程中将填充的token始终赋值为0向量
self.embedding = nn.Embedding(num_embeddings = vocab_size,embedding_dim = 3,padding_idx = 0)

# 卷积层, 用卷积提取特征, 并用池化层放大
self.conv = nn.Sequential()
self.conv.add_module("conv_1",nn.Conv1d(in_channels = 3,out_channels = 16,kernel_size = 5))
self.conv.add_module("pool_1",nn.MaxPool1d(kernel_size = 2))
self.conv.add_module("relu_1",nn.ReLU())
self.conv.add_module("conv_2",nn.Conv1d(in_channels = 16,out_channels = 128,kernel_size = 2))
self.conv.add_module("pool_2",nn.MaxPool1d(kernel_size = 2))
self.conv.add_module("relu_2",nn.ReLU())

# 全连接层
self.dense = nn.Sequential()
# 把卷积的输出摊平成一个长向量
self.dense.add_module("flatten",nn.Flatten())
# 把摊平后的向量压缩到一个线性值, 6144是摊平后的长度
self.dense.add_module("linear",nn.Linear(6144,1))

def forward(self,x):
x = self.embedding(x).transpose(1,2)
x = self.conv(x)
y = self.dense(x)
return y

net = Net()
print(net)

模型 summary 如下.

--------------------------------------------------------------------------
Layer (type) Output Shape Param #
==========================================================================
Embedding-1 [-1, 200, 3] 89,772
Conv1d-2 [-1, 16, 196] 256
MaxPool1d-3 [-1, 16, 98] 0
ReLU-4 [-1, 16, 98] 0
Conv1d-5 [-1, 128, 97] 4,224
MaxPool1d-6 [-1, 128, 48] 0
ReLU-7 [-1, 128, 48] 0
Flatten-8 [-1, 6144] 0
Linear-9 [-1, 1] 6,145
==========================================================================
Total params: 100,397
Trainable params: 100,397
Non-trainable params: 0
--------------------------------------------------------------------------
Input size (MB): 0.000076
Forward/backward pass size (MB): 0.287788
Params size (MB): 0.382984
Estimated Total Size (MB): 0.670849
--------------------------------------------------------------------------

训练模型

代码和前几次基本一模一样就不放了, 我本来以为每个模型都要捯饬一遍, 合着是套模板的, 草.

评估模型

似乎是过拟合了, val_acc 并不高.

%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import matplotlib.pyplot as plt

def plot_metric(dfhistory, metric):
train_metrics = dfhistory["train_"+metric]
val_metrics = dfhistory['val_'+metric]
epochs = range(1, len(train_metrics) + 1)
plt.plot(epochs, train_metrics, 'bo--')
plt.plot(epochs, val_metrics, 'ro-')
plt.title('Training and validation '+ metric)
plt.xlabel("Epochs")
plt.ylabel(metric)
plt.legend(["train_"+metric, 'val_'+metric])
plt.show()

使用和保存模型

def predict(net,dl):
net.eval()
with torch.no_grad():
result = nn.Sigmoid()(torch.cat([net.forward(t[0]) for t in dl]))
return(result.data)

net_clone = Net()
net_clone.load_state_dict(torch.load('checkpoint',weights_only=True))

总结

感觉得写个总结, 不然就是看一遍代码什么都没干.

文本模型的数据预处理包括:

  1. 去掉标点符号, 按空格分成单词
  2. 构建词典, 给每个词一个 token
  3. 添加<unk><pad>的对应 token
  4. padding 填充/切割成固定长度
  5. 构造数据管道, 便于把数据喂给模型

对处理文本的模型, 主要结构有:

  1. 嵌入层, 把 token 映射成向量, 把文本转换成矩阵
  2. 卷积层和池化层, 卷积类似小窗口提取关键特征, 池化压缩并保留重要信息
  3. 全连接层, 把卷积输出结果摊平成一个向量, 再把这个向量加权组合左右特征, 计算得到最终结果