准备数据

imdb数据集的目标是根据电影评论的文本内容预测评论的情感标签。
训练集有20000条电影评论文本,测试集有5000条电影评论文本,其中正面评论和负面评论都各占一半。
image.png
文本数据预处理较为繁琐,包括中文切词(本示例不涉及),构建词典,编码转换,序列填充,构建数据管道等等。
在torch中预处理文本数据一般使用torchtext或者自定义Dataset,torchtext功能非常强大,可以构建文本分类,序列标注,问答模型,机器翻译等NLP任务的数据集。

torchtext常见API一览:

  • torchtext.data.Example : 用来表示一个样本,数据和标签
  • torchtext.vocab.Vocab: 词汇表,可以导入一些预训练词向量
  • torchtext.data.Datasets: 数据集类,getitem返回 Example实例, torchtext.data.TabularDataset是其子类。
  • torchtext.data.Field : 用来定义字段的处理方法(文本字段,标签字段)创建 Example时的 预处理,batch 时的一些处理操作。
  • torchtext.data.Iterator: 迭代器,用来生成 batch
  • torchtext.datasets: 包含了常见的数据集
  1. import numpy as np
  2. import pandas as pd
  3. from collections import OrderedDict
  4. import re,string
  5. MAX_WORDS = 10000 # 仅考虑最高频的10000个词
  6. MAX_LEN = 200 # 每个样本保留200个词的长度
  7. BATCH_SIZE = 20
  8. train_data_path = '../data/imdb/train.tsv'
  9. test_data_path = '../data/imdb/test.tsv'
  10. train_token_path = '../data/imdb/train_token.tsv'
  11. test_token_path = '../data/imdb/test_token.tsv'
  12. train_samples_path = '../data/imdb/train_samples/'
  13. test_samples_path = '../data/imdb/test_samples/'

首先我们构建词典,并保留最高频的MAX_WORDS个词。

  1. ##构建词典
  2. word_count_dict = {}
  3. #清洗文本
  4. def clean_text(text):
  5. lowercase = text.lower().replace("\n"," ")
  6. stripped_html = re.sub('<br />', ' ',lowercase)
  7. cleaned_punctuation = re.sub('[%s]'%re.escape(string.punctuation),'',stripped_html)
  8. return cleaned_punctuation
  9. with open(train_data_path,"r",encoding = 'utf-8') as f:
  10. for line in f:
  11. label,text = line.split("\t")
  12. cleaned_text = clean_text(text)
  13. for word in cleaned_text.split(" "):
  14. word_count_dict[word] = word_count_dict.get(word,0)+1
  15. df_word_dict = pd.DataFrame(pd.Series(word_count_dict,name = "count"))
  16. df_word_dict = df_word_dict.sort_values(by = "count",ascending =False)
  17. df_word_dict = df_word_dict[0:MAX_WORDS-2] #
  18. df_word_dict["word_id"] = range(2,MAX_WORDS) #编号0和1分别留给未知词<unkown>和填充<padding>
  19. word_id_dict = df_word_dict["word_id"].to_dict()
  20. df_word_dict.head(10)

image.png

然后我们利用构建好的词典,将文本转换成token序号。

  1. #转换token
  2. # 填充文本
  3. def pad(data_list,pad_length):
  4. padded_list = data_list.copy()
  5. if len(data_list)> pad_length:
  6. padded_list = data_list[-pad_length:]
  7. if len(data_list)< pad_length:
  8. padded_list = [1]*(pad_length-len(data_list))+data_list
  9. return padded_list
  10. def text_to_token(text_file,token_file):
  11. with open(text_file,"r",encoding = 'utf-8') as fin,\
  12. open(token_file,"w",encoding = 'utf-8') as fout:
  13. for line in fin:
  14. label,text = line.split("\t")
  15. cleaned_text = clean_text(text)
  16. word_token_list = [word_id_dict.get(word, 0) for word in cleaned_text.split(" ")]
  17. pad_list = pad(word_token_list,MAX_LEN)
  18. out_line = label+"\t"+" ".join([str(x) for x in pad_list])
  19. fout.write(out_line+"\n")
  20. text_to_token(train_data_path,train_token_path)
  21. text_to_token(test_data_path,test_token_path)

接着将token文本按照样本分割,每个文件存放一个样本的数据。

  1. # 分割样本
  2. import os
  3. if not os.path.exists(train_samples_path):
  4. os.mkdir(train_samples_path)
  5. if not os.path.exists(test_samples_path):
  6. os.mkdir(test_samples_path)
  7. def split_samples(token_path,samples_dir):
  8. with open(token_path,"r",encoding = 'utf-8') as fin:
  9. i = 0
  10. for line in fin:
  11. with open(samples_dir+"%d.txt"%i,"w",encoding = "utf-8") as fout:
  12. fout.write(line)
  13. i = i+1
  14. split_samples(train_token_path,train_samples_path)
  15. split_samples(test_token_path,test_samples_path)
  1. print(os.listdir(train_samples_path)[0:100])

image.png

一切准备就绪,我们可以创建数据集Dataset, 从文件名称列表中读取文件内容了。

  1. import os
  2. from torch.utils.data import Dataset,DataLoader
  3. class imdbDataset(Dataset):
  4. def __init__(self,samples_dir):
  5. self.samples_dir = samples_dir
  6. self.samples_paths = os.listdir(samples_dir)
  7. def __len__(self):
  8. return len(self.samples_paths)
  9. def __getitem__(self,index):
  10. path = self.samples_dir + self.samples_paths[index]
  11. with open(path,"r",encoding = "utf-8") as f:
  12. line = f.readline()
  13. label,tokens = line.split("\t")
  14. label = torch.tensor([float(label)],dtype = torch.float)
  15. feature = torch.tensor([int(x) for x in tokens.split(" ")],dtype = torch.long)
  16. return (feature,label)
  1. ds_train = imdbDataset(train_samples_path)
  2. ds_test = imdbDataset(test_samples_path)
  3. print(len(ds_train))
  4. print(len(ds_test))
  5. dl_train = DataLoader(ds_train,batch_size = BATCH_SIZE,shuffle = True,num_workers=0)
  6. dl_test = DataLoader(ds_test,batch_size = BATCH_SIZE,num_workers=0)
  7. for features,labels in dl_train:
  8. print(features)
  9. print(labels)
  10. break

image.png


定义模型

使用Pytorch通常有三种方式构建模型:

  1. 使用nn.Sequential按层顺序构建模型;
  2. 继承nn.Module基类构建自定义模型;
  3. 继承nn.Module基类构建模型并辅助应用模型容器(nn.Sequential,nn.ModuleList,nn.ModuleDict)进行封装。

此处选择使用第三种方式进行构建。由于接下来使用类形式的训练循环,我们将模型封装成torchkeras.Model类来获得类似Keras中高阶模型接口的功能。Model类实际上继承自nn.Module类。
MacBook torchkeras包没导入成功,后边没实现。
把torchkears.py文件放到/Users/huang/miniforge3/lib/python3.9/site-packages是可以正常导入了,但后边训练部分还是报错。
torchkeras.py

  1. import torch
  2. from torch import nn
  3. import torchkeras
  1. torch.random.seed()
  2. import torch
  3. from torch import nn
  4. class Net(torchkeras.Model):
  5. def __init__(self):
  6. super(Net, self).__init__()
  7. #设置padding_idx参数后将在训练过程中将填充的token始终赋值为0向量
  8. self.embedding = nn.Embedding(num_embeddings = MAX_WORDS,embedding_dim = 3,padding_idx = 1)
  9. self.conv = nn.Sequential()
  10. self.conv.add_module("conv_1",nn.Conv1d(in_channels = 3,out_channels = 16,kernel_size = 5))
  11. self.conv.add_module("pool_1",nn.MaxPool1d(kernel_size = 2))
  12. self.conv.add_module("relu_1",nn.ReLU())
  13. self.conv.add_module("conv_2",nn.Conv1d(in_channels = 16,out_channels = 128,kernel_size = 2))
  14. self.conv.add_module("pool_2",nn.MaxPool1d(kernel_size = 2))
  15. self.conv.add_module("relu_2",nn.ReLU())
  16. self.dense = nn.Sequential()
  17. self.dense.add_module("flatten",nn.Flatten())
  18. self.dense.add_module("linear",nn.Linear(6144,1))
  19. self.dense.add_module("sigmoid",nn.Sigmoid())
  20. def forward(self,x):
  21. x = self.embedding(x).transpose(1,2)
  22. x = self.conv(x)
  23. y = self.dense(x)
  24. return y
  25. model = Net()
  26. print(model)
  27. model.summary(input_shape = (200,),input_dtype = torch.LongTensor)

image.png


训练模型

训练Pytorch通常需要用户编写自定义训练循环,训练循环的代码风格因人而异。
有3类典型的训练循环代码风格:

  1. 脚本形式训练循环;
  2. 函数形式训练循环;
  3. 类形式训练循环。

此处介绍一种类形式的训练循环。
我们仿照Keras定义了一个高阶的模型接口Model,实现 fit, validate,predict, summary 方法,相当于用户自定义高阶API。

  1. # 准确率
  2. def accuracy(y_pred,y_true):
  3. y_pred = torch.where(y_pred>0.5,torch.ones_like(y_pred,dtype = torch.float32),
  4. torch.zeros_like(y_pred,dtype = torch.float32))
  5. acc = torch.mean(1-torch.abs(y_true-y_pred))
  6. return acc
  7. model.compile(loss_func = nn.BCELoss(),optimizer= torch.optim.Adagrad(model.parameters(),lr = 0.02),
  8. metrics_dict={"accuracy":accuracy})
  1. # 有时候模型训练过程中不收敛,需要多试几次
  2. dfhistory = model.fit(20,dl_train,dl_val=dl_test,log_step_freq= 200)

image.png


评估模型

  1. %matplotlib inline
  2. %config InlineBackend.figure_format = 'svg'
  3. import matplotlib.pyplot as plt
  4. def plot_metric(dfhistory, metric):
  5. train_metrics = dfhistory[metric]
  6. val_metrics = dfhistory['val_'+metric]
  7. epochs = range(1, len(train_metrics) + 1)
  8. plt.plot(epochs, train_metrics, 'bo--')
  9. plt.plot(epochs, val_metrics, 'ro-')
  10. plt.title('Training and validation '+ metric)
  11. plt.xlabel("Epochs")
  12. plt.ylabel(metric)
  13. plt.legend(["train_"+metric, 'val_'+metric])
  14. plt.show()
  15. plot_metric(dfhistory,"loss")

image.png

  1. plot_metric(dfhistory,"accuracy")

image.png

  1. # 评估
  2. model.evaluate(dl_test)

image.png


使用模型

  1. model.predict(dl_test)

image.png


保存模型

推荐使用保存参数方式保存Pytorch模型。

  1. print(model.state_dict().keys())

image.png

  1. # 保存模型参数
  2. torch.save(model.state_dict(), "./data/model_parameter.pkl")
  3. model_clone = Net()
  4. model_clone.load_state_dict(torch.load("./data/model_parameter.pkl"))
  5. model_clone.compile(loss_func = nn.BCELoss(),optimizer= torch.optim.Adagrad(model.parameters(),lr = 0.02),
  6. metrics_dict={"accuracy":accuracy})
  7. # 评估模型
  8. model_clone.evaluate(dl_test)

image.png