当前位置:   article > 正文

pytorch实现IMDB数据集情感分类(全连接层的网络、LSTM)

imdb数据集

目录

 

一、任务描述

二、思路分析

三、准备数据集

3.1 基础dataset的准备

3.2 文本序列化

四、构建模型

4.1 仅有全连接层

4.2 LSTM

4.3 训练和测试

五、完整代码

5.1 全连接层实现分类完整代码

5.2 LSTM分类完整代码

5.3 测试结果


一、任务描述

使用Pytorch相关API,设计两种网络结构,一种网络结构中只有全连接层,一种使用文本处理中最为常用的LSTM,将数据集进行10分类,观察并对比两者的分类效果。

模型情感分类的数据集是经典的IMDB数据集,数据集下载地址:http://ai.stanford.edu/~amaas/data/sentiment/。这是一份包含了5万条流行电影的评论数据,其中训练集25000条,测试集25000条。数据格式如下:

数据的标签以文件名的方式呈现,图中左边为名称,其中名称包含两部分,分别是序号和情感评分,即序号_情感评分。情感评分中1-4为neg,5-10为pos,共有10个分类。右边为文件中的评论内容。每个文件中文本长度不一定相等。

数据集的组织形式如下:

下载完数据集,在aclImdb文件夹中,有如下文件:

traintest分别表示训练数据测试数据所在的文件夹,其中文件夹中的内容如下:

随意点开一个neg/pos,文件夹中都是txt文件,每个文件代表一条样本数据:

这些路径在后续写代码的过程中都会用得到,因为需要根据路径来读取每个txt文件。

 

二、思路分析

具体可以细分为如下几步:

  1. 准备数据集,实例化dataset,准备dataloader,即设计一个类来获取样本

  2. 构建模型,定义模型多少层、形状变化、用什么激活函数等

  3. 模型训练,观察迭代过程中的损失

  4. 模型评估,观察分类的准确率

这里再着重考虑一下评论文本该怎么表示:首先评论的文本需要分词处理,处理成一个个单词,但是由于评论有长有短,所以这里需要统一长度,假设每条评论文本都有max_len个词,比50个词长的文本进行截取操作,比50个词短的文本则填充到50。接着是关于词的表示,我们用word embedding,即词向量的形式表示一个词,词向量的维度是embedding dim。这里词向量是调用pytorch中nn.Embedding方法实现的,按照给定的词向量的维度,该方法会给每个词一个随机的词向量,当然这种方法的词向量肯定不太好,nn.Embedding方法还可以加载预训练好的词向量,比如glove,word2vec等,感兴趣的可以尝试一下。

nn.Embedding方法除了需要指定embedding dim这个参数以外,它是基于一个已有的词典来随机分配词向量的,所以我们还需要从训练数据中构建一个词典,词典的大小是训练样本中的所有词,构建过程下文中会讲到,构建了这个词典后,每个词在词典中都会映射到一个特有的,用数字表示的ID上,比如hello这个词在词典中对应的是367,world对应897。nn.Embedding方法就是按照这个来分配每个词的随机向量表示的。构建完成后,比如在测试阶段,我们也需要对测试样本进行处理,并得到向量表示,这样才能喂给神经网络得到预测结果,此时要是一个词在训练样本从没有出现过,那么这个词在词典中就找不到对应的数字表示了,所以构建词典的时候我们指定一个特殊的值"UNK",其值是0,也就是说,没出现过的词,都映射为0。前面我们还说过评论需要统一长度,填充词也被预先定义成“PAD”,其值是1。

比如对于一个测试样本,分词后得到:["ni", "hao", "shi", "jie"],其中ni和jie在训练样本中出现过,它们的ID分别为34和90,hao和shi没有出现,假设max_len为5那么["ni", "hao", "shi", "jie"]可以表示为:[34, 0, 0, 90, 1],然后每个词都转换为词向量,喂给神经网络,得到输出与实际结果进行比较,观察是否正确分类。

上面叙述了一个大概的流程,具体的过程在下面会详细提到。

 

三、准备数据集

准备数据集和之前的方法一样,实例化dataset,准备dataloader,最终我们的数据可以处理成如下格式:

图中示意的是batch_size等于2的情况,也就是说dataloader一次只加载两个样本。其中[4,6]是两个样本的情感标签,后面的text是两个样本中分词得到的内容,形式为(['token1', 'token2'...],['token1', 'token2'...]),元组中每个列表对应一个样本,所以每个[]中共有max_len个token。

 

其中关键点在于:

  1. 如何完成基础Dataset的构建和Dataloader的准备

  2. 每个batch中文本的长度不一致的问题如何解决

  3. 每个batch中的文本如何转化为数字序列

3.1 基础dataset的准备

  1. import torch
  2. from torch.utils.data import DataLoader,Dataset
  3. import os
  4. import re
  5. # 路径需要根据情况修改,文件太大的时候可以引用绝对路径
  6. data_base_path = r"data\aclImdb"
  7. #1. 定义tokenize的方法,对评论文本分词
  8. def tokenize(text):
  9. # fileters = '!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n'
  10. fileters = ['!','"','#','$','%','&','\(','\)','\*','\+',',','-','\.','/',':',';','<','=','>','\?','@'
  11. ,'\[','\\','\]','^','_','`','\{','\|','\}','~','\t','\n','\x97','\x96','”','“',]
  12. # sub方法是替换
  13. text = re.sub("<.*?>"," ",text,flags=re.S) # 去掉<...>中间的内容,主要是文本内容中存在<br/>等内容
  14. text = re.sub("|".join(fileters)," ",text,flags=re.S) # 替换掉特殊字符,'|'是把所有要匹配的特殊字符连在一起
  15. return [i.strip() for i in text.split()] # 去掉前后多余的空格
  16. #2. 准备dataset
  17. class ImdbDataset(Dataset):
  18. def __init__(self,mode):
  19. super(ImdbDataset,self).__init__()
  20. # 读取所有的训练文件夹名称
  21. if mode=="train":
  22. text_path = [os.path.join(data_base_path,i) for i in ["train/neg","train/pos"]]
  23. else:
  24. text_path = [os.path.join(data_base_path,i) for i in ["test/neg","test/pos"]]
  25. self.total_file_path_list = []
  26. # 进一步获取所有文件的名称
  27. for i in text_path:
  28. self.total_file_path_list.extend([os.path.join(i,j) for j in os.listdir(i)])
  29. def __getitem__(self, idx):
  30. cur_path = self.total_file_path_list[idx]
  31. # 返回path最后的文件名。如果path以/或\结尾,那么就会返回空值。即os.path.split(path)的第二个元素。
  32. # cur_filename返回的是如:“0_3.txt”的文件名
  33. cur_filename = os.path.basename(cur_path)
  34. # 标题的形式是:3_4.txt 前面的3是索引,后面的4是分类
  35. # 原本的分类是1-10,现在变为0-9
  36. label = int(cur_filename.split("_")[-1].split(".")[0]) -1 #处理标题,获取label,-1是因为要转化为[0-9]
  37. text = tokenize(open(cur_path).read().strip()) #直接按照空格进行分词
  38. return label,text
  39. def __len__(self):
  40. return len(self.total_file_path_list)
  41. # 测试是否能成功获取数据
  42. dataset = ImdbDataset(mode="train")
  43. print(dataset[0])
  44. # out:(2, ['Story', 'of', 'a', 'man', 'who', 'has', 'unnatural', 'feelings'...])
  45. # 2. 实例化,准备dataloader
  46. dataset = ImdbDataset(mode="train")
  47. dataloader = DataLoader(dataset=dataset,batch_size=2,shuffle=True)
  48. #3. 观察数据输出结果,在pytorch新版本(1.6)中,这里已经不能运行了,需要加上下面的`collate_fn`函数来运行,即使能够运行,结果也是不正确的
  49. for idx,(label,text) in enumerate(dataloader):
  50. print("idx:",idx)
  51. print("lable:",label)
  52. print("text:",text)
  53. break

输出如下:

  1. idx: 0
  2. table: tensor([3, 1])
  3. text: [('I', 'Want'), ('thought', 'a'), ('this', 'great'), ('was', 'recipe'), ('a', 'for'), ('great', 'failure'), ('idea', 'Take'), ('but', 'a'), ('boy', 's'), ('was', 'y'), ('it', 'plot'), ('poorly', 'add'), ('executed', 'in'), ('We', 'some'), ('do', 'weak'), ('get', 'completely'), ('a', 'undeveloped'), ('broad', 'characters'), ('sense', 'and'), ('of', 'than'), ('how', 'throw'), ('complex', 'in'), ('and', 'the'), ('challenging', 'worst'), ('the', 'special'), ('backstage', 'effects'), ('operations', 'a'), ('of', 'horror'), ('a', 'movie'), ('show', 'has'), ('are', 'known'), ('but', 'Let'), ('virtually', 'stew'), ('no', 'for'), ...('show', 'somehow'), ('rather', 'destroy'), ('than', 'every'), ('anything', 'copy'), ('worth', 'of'), ('watching', 'this'), ('for', 'film'), ('its', 'so'), ('own', 'it'), ('merit', 'will')]

很明显这里出现了问题,我们希望的是(['token1', 'token2'...],['token1', 'token2'...])的形式,但是结果中却把单词两两组合了。出现问题的原因在于Dataloader中的参数collate_fn,collate_fn的默认值为torch自定义的default_collate,collate_fn的作用就是对每个batch进行处理,而默认的default_collate处理出错。

在默认定义的collate_fn方法中,有一个参数batch,值为([tokens, label], [tokens, label])。也就是根据你的batch_size,决定元组中有多少个item,默认的collate_fn方法对batch做一个zip操作,把两个输入的item组合在一起,把两个目标值组合在一起,但是这里的输入是['Story', 'of', 'a', 'man', 'who', 'has', 'unnatural']是这样的形式,会再进行一次zip操作,多进行了一次两两组合(!!!),但是这显然不是我们想要的。前面的手写数字识别的程序中,由于图片用像素表示,而且我们已经将图片转换为tensor了,所以不会出错。

那么怎么才能获取到正确结果呢?

方法1:考虑先把数据转化为数字序列,观察其结果是否符合要求,之前使用DataLoader并未出现类似错误

方法2:考虑自定义一个collate_fn,观察结果

这里使用方式2,自定义一个collate_fn,然后观察结果:

  1. # 自定义的collate_fn方法
  2. def collate_fn(batch):
  3. # 手动zip操作,并转换为list,否则无法获取文本和标签了
  4. batch = list(zip(*batch))
  5. labels = torch.tensor(batch[0], dtype=torch.int32)
  6. texts = batch[1]
  7. texts = torch.tensor([ws.transform(i, max_len) for i in texts])
  8. del batch
  9. # 注意这里long()不可少,否则会报错
  10. return labels.long(), texts.long()
  11. #此时输出正常
  12. for idx,(label,text) in enumerate(dataloader):
  13. print("idx:",idx)
  14. print("label:",label)
  15. print("text:",text)
  16. break
  17. # table: tensor([2, 9], dtype=torch.int32) 2, 9是两个分类
  18. # text:([], [])

最后我们可以准备一个get_dataloader方法,更方便的获取数据:

  1. # 获取数据的方法
  2. def get_dataloader(train=True):
  3. if train:
  4. mode = 'train'
  5. else:
  6. mode = "test"
  7. dataset = ImdbDataset(mode)
  8. batch_size = train_batch_size if train else test_batch_size
  9. return DataLoader(dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)

3.2 文本序列化

我们这里使用的word embedding,不会直接把文本转化为向量,而是先转化为数字,再把数字转化为向量,那么这个过程该如何实现呢?

这里我们可以考虑把文本中的每个词语和其对应的数字,使用字典保存,同时实现方法把句子通过字典映射为包含数字的列表

实现文本序列化之前,考虑以下几点:

  1. 如何使用字典把词语和数字进行对应

  2. 不同的词语出现的次数不尽相同,是否需要对高频或者低频词语进行过滤,以及总的词语数量是否需要进行限制

  3. 得到词典之后,如何把句子转化为数字序列

  4. 不同句子长度不相同,每个batch的句子如何构造成相同的长度(可以对短句子进行填充,填充特殊字符)

  5. 对于新出现的词语在词典中没有出现怎么办(可以使用特殊字符代替

思路分析:

  1. 对所有句子进行分词

  2. 词语存入字典,根据次数对词语进行过滤,并统计次数

  3. 实现文本转数字序列的方法

  4. 实现数字序列转文本方法(其实该任务中用不到这个方法)

  1. # Word2Sequence
  2. class Word2Sequence:
  3. # 未出现过的词
  4. UNK_TAG = "UNK"
  5. PAD_TAG = "PAD"
  6. # 填充的词
  7. UNK = 0
  8. PAD = 1
  9. def __init__(self):
  10. self.dict = {
  11. self.UNK_TAG: self.UNK,
  12. self.PAD_TAG: self.PAD
  13. }
  14. self.count = {}
  15. def to_index(self, word):
  16. """word -> index"""
  17. return self.dict.get(word, self.UNK)
  18. def to_word(self, index):
  19. """index -> word"""
  20. if index in self.inversed_dict:
  21. return self.inversed_dict[index]
  22. return self.UNK_TAG
  23. def __len__(self):
  24. return len(self.dict)
  25. def fit(self, sentence):
  26. """count字典中存储每个单词出现的次数"""
  27. for word in sentence:
  28. self.count[word] = self.count.get(word, 0) + 1
  29. def build_vocab(self, min_count=None, max_count=None, max_feature=None):
  30. """
  31. 构建词典
  32. 只筛选出现次数在[min_count,max_count]之间的词
  33. 词典最大的容纳的词为max_feature,按照出现次数降序排序,要是max_feature有规定,出现频率很低的词就被舍弃了
  34. """
  35. if min_count is not None:
  36. self.count = {word: count for word, count in self.count.items() if count >= min_count}
  37. if max_count is not None:
  38. self.count = {word: count for word, count in self.count.items() if count <= max_count}
  39. if max_feature is not None:
  40. self.count = dict(sorted(self.count.items(), lambda x: x[-1], reverse=True)[:max_feature])
  41. # 给词典中每个词分配一个数字ID
  42. for word in self.count:
  43. self.dict[word] = len(self.dict)
  44. # 构建一个数字映射到单词的词典,方法反向转换,但程序中用不太到
  45. self.inversed_dict = dict(zip(self.dict.values(), self.dict.keys()))
  46. def transform(self, sentence, max_len=None):
  47. """
  48. 根据词典给每个词分配的数字ID,将给定的sentence(字符串序列)转换为数字序列
  49. max_len:统一文本的单词个数
  50. """
  51. if max_len is not None:
  52. r = [self.PAD] * max_len
  53. else:
  54. r = [self.PAD] * len(sentence)
  55. # 截断文本
  56. if max_len is not None and len(sentence) > max_len:
  57. sentence = sentence[:max_len]
  58. for index, word in enumerate(sentence):
  59. r[index] = self.to_index(word)
  60. return np.array(r, dtype=np.int64)
  61. def inverse_transform(self, indices):
  62. """数字序列-->单词序列"""
  63. sentence = []
  64. for i in indices:
  65. word = self.to_word(i)
  66. sentence.append(word)
  67. return sentence

定义完这个类之后,可以简单测试一下效果:

  1. # 测试Word2Sequence
  2. w2s = Word2Sequence()
  3. voc = [["你", "好", "么"],
  4. ["你", "好", "哦"]]
  5. for i in voc:
  6. w2s.fit(i)
  7. w2s.build_vocab()
  8. print(w2s.dict)
  9. print(w2s.transform(["你", "好", "嘛"]))

结果如下:

  1. {'UNK': 0, 'PAD': 1, '你': 2, '好': 3, '么': 4, '哦': 5}
  2. [2 3 0]

功能经测试正确,那么我们就可以用训练文本构建词典了,注意不能读取测试样本

  1. # 建立词表
  2. def fit_save_word_sequence():
  3. word_to_sequence = Word2Sequence()
  4. train_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
  5. # total_file_path_list存储总的需要读取的txt文件
  6. total_file_path_list = []
  7. for i in train_path:
  8. total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
  9. # tqdm是显示进度条的
  10. for cur_path in tqdm(total_file_path_list, ascii=True, desc="fitting"):
  11. word_to_sequence.fit(tokenize(open(cur_path, encoding="utf-8").read().strip()))
  12. word_to_sequence.build_vocab()
  13. # 对wordSequesnce进行保存
  14. pickle.dump(word_to_sequence, open("model/ws.pkl", "wb"))

执行该方法:

    fit_save_word_sequence()

结果会生成如下文件,注意,model文件夹需要事先创建!

文件生成成功后,可以使用如下的代码加载,ws就是Word2Sequence类的一个实例,在后续工作中我们会用到。

ws = pickle.load(open("./model/ws.pkl", "rb"))

 

四、构建模型

4.1 仅有全连接层

  1. class IMDBModel(nn.Module):
  2. def __init__(self):
  3. # 定义了两个全连接层,其中最后一个全连接层使用了softmax激活函数,并将输入的维度转换为10,实现10分类
  4. super(IMDBModel, self).__init__()
  5. # nn.Embedding方法参数:
  6. # len(ws):词典的总的词的数量。
  7. # 300:词向量的维度,即embedding dim
  8. # padding_idx,填充词
  9. self.embedding = nn.Embedding(len(ws), 300, padding_idx=ws.PAD)
  10. self.fc1 = nn.Linear(max_len * 300, 128)
  11. self.fc = nn.Linear(128, 10)
  12. def forward(self, x):
  13. embed = self.embedding(x)
  14. embed = embed.view(x.size(0), -1)
  15. out = self.fc1(embed)
  16. out = F.relu(out)
  17. out = self.fc(out)
  18. return F.log_softmax(out, dim=-1)

 

4.2 LSTM

关于pytorch中LSTM api的使用,已经输入输出的理解,可以参考我之前写的博客:

万字长文:深入理解各类型神经网络(简单神经网络,CNN,LSTM)的输入和输出

  1. class IMDBModel(nn.Module):
  2. def __init__(self):
  3. super(IMDBModel, self).__init__()
  4. self.hidden_size = 64
  5. self.embedding_dim = 200
  6. self.num_layer = 2
  7. self.bidirectional = True
  8. self.bi_num = 2 if self.bidirectional else 1
  9. self.dropout = 0.5
  10. # 以上部分为超参数,可以自行修改
  11. self.embedding = nn.Embedding(len(ws), self.embedding_dim, padding_idx=ws.PAD)
  12. self.lstm = nn.LSTM(self.embedding_dim, self.hidden_size,
  13. self.num_layer, bidirectional=True, dropout=self.dropout)
  14. self.fc = nn.Linear(self.hidden_size * self.bi_num, 20)
  15. self.fc2 = nn.Linear(20, 10)
  16. def forward(self, x):
  17. x = self.embedding(x)
  18. x = x.permute(1, 0, 2) # 进行轴交换
  19. h_0, c_0 = self.init_hidden_state(x.size(1))
  20. _, (h_n, c_n) = self.lstm(x, (h_0, c_0))
  21. # 只要最后一个lstm单元处理的结果,取前向LSTM和后向LSTM的结果进行简单拼接
  22. out = torch.cat([h_n[-2, :, :], h_n[-1, :, :]], dim=-1)
  23. out = self.fc(out)
  24. out = F.relu(out)
  25. out = self.fc2(out)
  26. return F.log_softmax(out, dim=-1)
  27. def init_hidden_state(self, batch_size):
  28. h_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
  29. c_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
  30. return h_0, c_0

4.3 训练和测试

首先指定一些超参数,放在程序的最前面:

  1. train_batch_size = 512
  2. test_batch_size = 500
  3. max_len = 50

训练和测试:

  1. def train(epoch):
  2. mode = True
  3. train_dataloader = get_dataloader(mode)
  4. for idx, (target, input) in enumerate(train_dataloader):
  5. optimizer.zero_grad()
  6. output = imdb_model(input)
  7. loss = F.nll_loss(output, target)
  8. loss.backward()
  9. optimizer.step()
  10. if idx % 10 == 0:
  11. print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
  12. epoch, idx * len(input), len(train_dataloader.dataset),
  13. 100. * idx / len(train_dataloader), loss.item()))
  14. torch.save(imdb_model.state_dict(), "model/mnist_net.pkl")
  15. torch.save(optimizer.state_dict(), 'model/mnist_optimizer.pkl')
  16. def test():
  17. test_loss = 0
  18. correct = 0
  19. mode = False
  20. imdb_model.eval()
  21. test_dataloader = get_dataloader(mode)
  22. with torch.no_grad():
  23. for target, input in test_dataloader:
  24. output = imdb_model(input)
  25. test_loss += F.nll_loss(output, target, reduction="sum")
  26. pred = torch.max(output, dim=-1, keepdim=False)[-1]
  27. correct += pred.eq(target.data).sum()
  28. test_loss = test_loss / len(test_dataloader.dataset)
  29. print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
  30. test_loss, correct, len(test_dataloader.dataset),
  31. 100. * correct / len(test_dataloader.dataset)))
  1. if __name__ == '__main__':
  2. # # 测试数据集的功能
  3. # dataset = ImdbDataset(mode="train")
  4. # dataloader = DataLoader(dataset=dataset, batch_size=2, shuffle=True, collate_fn=collate_fn)
  5. # for idx, (label, text) in enumerate(dataloader):
  6. # print("idx:", idx)
  7. # print("lable:", label)
  8. # print("text:", text)
  9. # break
  10. # 测试Word2Sequence
  11. # w2s = Word2Sequence()
  12. # voc = [["你", "好", "么"],
  13. # ["你", "好", "哦"]]
  14. # for i in voc:
  15. # w2s.fit(i)
  16. # w2s.build_vocab()
  17. # print(w2s.dict)
  18. # print(w2s.transform(["你", "好", "嘛"]))
  19. fit_save_word_sequence()
  20. # 训练和测试
  21. test()
  22. for i in range(3):
  23. train(i)
  24. print(
  25. "训练第{}轮的测试结果-----------------------------------------------------------------------------------------".format(
  26. i + 1))
  27. test()

其中两个不同网络结构训练和测试的代码是相同的,不过在LSTM网络中,有一处参数的变化

train_batch_size = 64

五、完整代码

需要特别注意的是,中间有一行代码:

ws = pickle.load(open("./model/ws.pkl", "rb"))

这行代码得建立词库才能正常执行,也就是必须有这个文件才能不报错,所以在训练和测试之前,先执行:

fit_save_word_sequence()

5.1 全连接层实现分类完整代码

  1. import torch
  2. from torch.utils.data import DataLoader, Dataset
  3. import os
  4. import re
  5. import numpy as np
  6. import pickle
  7. from tqdm import tqdm
  8. import torch.nn as nn
  9. import torch.nn.functional as F
  10. from torch import optim
  11. data_base_path = r'E:\2020.1.16\BaiduNetdiskDownload\python5.0\课件资' \
  12. r'料V5.0解压密码:www.hoh0.com\课件资料V5.0\阶段9-人工智' \
  13. r'能NLP项目\第四天\代码\data\aclImdb_v1\aclImdb'
  14. train_batch_size = 512
  15. test_batch_size = 500
  16. max_len = 50
  17. # 分词的API
  18. def tokenize(text):
  19. # fileters = '!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n'
  20. fileters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>',
  21. '\?', '@', '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '”',
  22. '“', ]
  23. text = re.sub("<.*?>", " ", text, flags=re.S)
  24. text = re.sub("|".join(fileters), " ", text, flags=re.S)
  25. return [i.strip() for i in text.split()]
  26. # 自定义的数据集
  27. class ImdbDataset(Dataset):
  28. def __init__(self, mode):
  29. super(ImdbDataset, self).__init__()
  30. if mode == "train":
  31. text_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
  32. else:
  33. text_path = [os.path.join(data_base_path, i) for i in ["test/neg", "test/pos"]]
  34. self.total_file_path_list = []
  35. for i in text_path:
  36. self.total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
  37. # print(self.total_file_path_list)
  38. def __getitem__(self, idx):
  39. cur_path = self.total_file_path_list[idx]
  40. cur_filename = os.path.basename(cur_path)
  41. label = int(cur_filename.split("_")[-1].split(".")[0]) - 1
  42. text = tokenize(open(cur_path, encoding="utf-8").read().strip())
  43. return label, text
  44. def __len__(self):
  45. return len(self.total_file_path_list)
  46. # 自定义的collate_fn方法
  47. def collate_fn(batch):
  48. # 手动zip操作,并转换为list,否则无法获取文本和标签了
  49. batch = list(zip(*batch))
  50. labels = torch.tensor(batch[0], dtype=torch.int32)
  51. texts = batch[1]
  52. texts = torch.tensor([ws.transform(i, max_len) for i in texts])
  53. del batch
  54. # 注意这里long()不可少,否则会报错
  55. return labels.long(), texts.long()
  56. # 获取数据的方法
  57. def get_dataloader(train=True):
  58. if train:
  59. mode = 'train'
  60. else:
  61. mode = "test"
  62. dataset = ImdbDataset(mode)
  63. batch_size = train_batch_size if train else test_batch_size
  64. return DataLoader(dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
  65. # Word2Sequence
  66. class Word2Sequence:
  67. # 未出现过的词
  68. UNK_TAG = "UNK"
  69. PAD_TAG = "PAD"
  70. # 填充的词
  71. UNK = 0
  72. PAD = 1
  73. def __init__(self):
  74. self.dict = {
  75. self.UNK_TAG: self.UNK,
  76. self.PAD_TAG: self.PAD
  77. }
  78. self.count = {}
  79. def to_index(self, word):
  80. """word -> index"""
  81. return self.dict.get(word, self.UNK)
  82. def to_word(self, index):
  83. """index -> word"""
  84. if index in self.inversed_dict:
  85. return self.inversed_dict[index]
  86. return self.UNK_TAG
  87. def __len__(self):
  88. return len(self.dict)
  89. def fit(self, sentence):
  90. """count字典中存储每个单词出现的次数"""
  91. for word in sentence:
  92. self.count[word] = self.count.get(word, 0) + 1
  93. def build_vocab(self, min_count=None, max_count=None, max_feature=None):
  94. """
  95. 构建词典
  96. 只筛选出现次数在[min_count,max_count]之间的词
  97. 词典最大的容纳的词为max_feature,按照出现次数降序排序,要是max_feature有规定,出现频率很低的词就被舍弃了
  98. """
  99. if min_count is not None:
  100. self.count = {word: count for word, count in self.count.items() if count >= min_count}
  101. if max_count is not None:
  102. self.count = {word: count for word, count in self.count.items() if count <= max_count}
  103. if max_feature is not None:
  104. self.count = dict(sorted(self.count.items(), lambda x: x[-1], reverse=True)[:max_feature])
  105. # 给词典中每个词分配一个数字ID
  106. for word in self.count:
  107. self.dict[word] = len(self.dict)
  108. # 构建一个数字映射到单词的词典,方法反向转换,但程序中用不太到
  109. self.inversed_dict = dict(zip(self.dict.values(), self.dict.keys()))
  110. def transform(self, sentence, max_len=None):
  111. """
  112. 根据词典给每个词分配的数字ID,将给定的sentence(字符串序列)转换为数字序列
  113. max_len:统一文本的单词个数
  114. """
  115. if max_len is not None:
  116. r = [self.PAD] * max_len
  117. else:
  118. r = [self.PAD] * len(sentence)
  119. # 截断文本
  120. if max_len is not None and len(sentence) > max_len:
  121. sentence = sentence[:max_len]
  122. for index, word in enumerate(sentence):
  123. r[index] = self.to_index(word)
  124. return np.array(r, dtype=np.int64)
  125. def inverse_transform(self, indices):
  126. """数字序列-->单词序列"""
  127. sentence = []
  128. for i in indices:
  129. word = self.to_word(i)
  130. sentence.append(word)
  131. return sentence
  132. # 建立词表
  133. def fit_save_word_sequence():
  134. word_to_sequence = Word2Sequence()
  135. train_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
  136. # total_file_path_list存储总的需要读取的txt文件
  137. total_file_path_list = []
  138. for i in train_path:
  139. total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
  140. # tqdm是显示进度条的
  141. for cur_path in tqdm(total_file_path_list, ascii=True, desc="fitting"):
  142. word_to_sequence.fit(tokenize(open(cur_path, encoding="utf-8").read().strip()))
  143. word_to_sequence.build_vocab()
  144. # 对wordSequesnce进行保存
  145. pickle.dump(word_to_sequence, open("model/ws.pkl", "wb"))
  146. ws = pickle.load(open("./model/ws.pkl", "rb"))
  147. # print(len(ws))
  148. # 模型
  149. class IMDBModel(nn.Module):
  150. def __init__(self):
  151. # 定义了两个全连接层,其中最后一个全连接层使用了softmax激活函数,并将输入的维度转换为10,实现10分类
  152. super(IMDBModel, self).__init__()
  153. # nn.Embedding方法参数:
  154. # len(ws):词典的总的词的数量。
  155. # 300:词向量的维度,即embedding dim
  156. # padding_idx,填充词
  157. self.embedding = nn.Embedding(len(ws), 300, padding_idx=ws.PAD)
  158. self.fc1 = nn.Linear(max_len * 300, 128)
  159. self.fc = nn.Linear(128, 10)
  160. def forward(self, x):
  161. embed = self.embedding(x)
  162. embed = embed.view(x.size(0), -1)
  163. out = self.fc1(embed)
  164. out = F.relu(out)
  165. out = self.fc(out)
  166. return F.log_softmax(out, dim=-1)
  167. # 实例化
  168. imdb_model = IMDBModel()
  169. # 优化器
  170. optimizer = optim.Adam(imdb_model.parameters())
  171. # 交叉熵损失
  172. criterion = nn.CrossEntropyLoss()
  173. def train(epoch):
  174. mode = True
  175. train_dataloader = get_dataloader(mode)
  176. for idx, (target, input) in enumerate(train_dataloader):
  177. optimizer.zero_grad()
  178. output = imdb_model(input)
  179. loss = F.nll_loss(output, target)
  180. loss.backward()
  181. optimizer.step()
  182. if idx % 10 == 0:
  183. print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
  184. epoch, idx * len(input), len(train_dataloader.dataset),
  185. 100. * idx / len(train_dataloader), loss.item()))
  186. torch.save(imdb_model.state_dict(), "model/mnist_net.pkl")
  187. torch.save(optimizer.state_dict(), 'model/mnist_optimizer.pkl')
  188. def test():
  189. test_loss = 0
  190. correct = 0
  191. mode = False
  192. imdb_model.eval()
  193. test_dataloader = get_dataloader(mode)
  194. with torch.no_grad():
  195. for target, input in test_dataloader:
  196. output = imdb_model(input)
  197. test_loss += F.nll_loss(output, target, reduction="sum")
  198. pred = torch.max(output, dim=-1, keepdim=False)[-1]
  199. correct += pred.eq(target.data).sum()
  200. test_loss = test_loss / len(test_dataloader.dataset)
  201. print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
  202. test_loss, correct, len(test_dataloader.dataset),
  203. 100. * correct / len(test_dataloader.dataset)))
  204. if __name__ == '__main__':
  205. # # 测试数据集的功能
  206. # dataset = ImdbDataset(mode="train")
  207. # dataloader = DataLoader(dataset=dataset, batch_size=2, shuffle=True, collate_fn=collate_fn)
  208. # for idx, (label, text) in enumerate(dataloader):
  209. # print("idx:", idx)
  210. # print("lable:", label)
  211. # print("text:", text)
  212. # break
  213. # 测试Word2Sequence
  214. # w2s = Word2Sequence()
  215. # voc = [["你", "好", "么"],
  216. # ["你", "好", "哦"]]
  217. # for i in voc:
  218. # w2s.fit(i)
  219. # w2s.build_vocab()
  220. # print(w2s.dict)
  221. # print(w2s.transform(["你", "好", "嘛"]))
  222. # fit_save_word_sequence()
  223. # 训练和测试
  224. test()
  225. for i in range(3):
  226. train(i)
  227. print(
  228. "训练第{}轮的测试结果-----------------------------------------------------------------------------------------".format(
  229. i + 1))
  230. test()

5.2 LSTM分类完整代码

其中有些注释参照上面的程序,两段代码只有模型定义和一个train_batch_size不相同

  1. import torch
  2. from torch.utils.data import DataLoader, Dataset
  3. import os
  4. import re
  5. import numpy as np
  6. import pickle
  7. from tqdm import tqdm
  8. import torch.nn as nn
  9. import torch.nn.functional as F
  10. from torch import optim
  11. data_base_path = r'E:\2020.1.16\BaiduNetdiskDownload\python5.0\课件资' \
  12. r'料V5.0解压密码:www.hoh0.com\课件资料V5.0\阶段9-人工智' \
  13. r'能NLP项目\第四天\代码\data\aclImdb_v1\aclImdb'
  14. train_batch_size = 64
  15. test_batch_size = 500
  16. max_len = 50
  17. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  18. # 分词的API
  19. def tokenize(text):
  20. # fileters = '!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n'
  21. fileters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>',
  22. '\?', '@', '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '”',
  23. '“', ]
  24. text = re.sub("<.*?>", " ", text, flags=re.S)
  25. text = re.sub("|".join(fileters), " ", text, flags=re.S)
  26. return [i.strip() for i in text.split()]
  27. # 自定义的数据集
  28. class ImdbDataset(Dataset):
  29. def __init__(self, mode):
  30. super(ImdbDataset, self).__init__()
  31. if mode == "train":
  32. text_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
  33. else:
  34. text_path = [os.path.join(data_base_path, i) for i in ["test/neg", "test/pos"]]
  35. self.total_file_path_list = []
  36. for i in text_path:
  37. self.total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
  38. # print(self.total_file_path_list)
  39. def __getitem__(self, idx):
  40. cur_path = self.total_file_path_list[idx]
  41. cur_filename = os.path.basename(cur_path)
  42. label = int(cur_filename.split("_")[-1].split(".")[0]) - 1
  43. text = tokenize(open(cur_path, encoding="utf-8").read().strip())
  44. return label, text
  45. def __len__(self):
  46. return len(self.total_file_path_list)
  47. # 自定义的collate_fn方法
  48. def collate_fn(batch):
  49. batch = list(zip(*batch))
  50. labels = torch.tensor(batch[0], dtype=torch.int32)
  51. texts = batch[1]
  52. texts = torch.tensor([ws.transform(i, max_len) for i in texts])
  53. del batch
  54. return labels.long(), texts.long()
  55. # 获取数据的方法
  56. def get_dataloader(train=True):
  57. if train:
  58. mode = 'train'
  59. else:
  60. mode = "test"
  61. dataset = ImdbDataset(mode)
  62. batch_size = train_batch_size if train else test_batch_size
  63. return DataLoader(dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
  64. # Word2Sequence
  65. class Word2Sequence:
  66. UNK_TAG = "UNK"
  67. PAD_TAG = "PAD"
  68. UNK = 0
  69. PAD = 1
  70. def __init__(self):
  71. self.dict = {
  72. self.UNK_TAG: self.UNK,
  73. self.PAD_TAG: self.PAD
  74. }
  75. self.fited = False
  76. self.count = {}
  77. def to_index(self, word):
  78. return self.dict.get(word, self.UNK)
  79. def to_word(self, index):
  80. if index in self.inversed_dict:
  81. return self.inversed_dict[index]
  82. return self.UNK_TAG
  83. def __len__(self):
  84. return len(self.dict)
  85. def fit(self, sentence):
  86. for word in sentence:
  87. self.count[word] = self.count.get(word, 0) + 1
  88. def build_vocab(self, min_count=None, max_count=None, max_feature=None):
  89. if min_count is not None:
  90. self.count = {word: count for word, count in self.count.items() if count >= min_count}
  91. if max_count is not None:
  92. self.count = {word: count for word, count in self.count.items() if count <= max_count}
  93. if max_feature is not None:
  94. self.count = dict(sorted(self.count.items(), lambda x: x[-1], reverse=True)[:max_feature])
  95. for word in self.count:
  96. self.dict[word] = len(self.dict)
  97. self.inversed_dict = dict(zip(self.dict.values(), self.dict.keys()))
  98. def transform(self, sentence, max_len=None):
  99. if max_len is not None:
  100. r = [self.PAD] * max_len
  101. else:
  102. r = [self.PAD] * len(sentence)
  103. if max_len is not None and len(sentence) > max_len:
  104. sentence = sentence[:max_len]
  105. for index, word in enumerate(sentence):
  106. r[index] = self.to_index(word)
  107. return np.array(r, dtype=np.int64)
  108. def inverse_transform(self, indices):
  109. sentence = []
  110. for i in indices:
  111. word = self.to_word(i)
  112. sentence.append(word)
  113. return sentence
  114. # 建立词表
  115. def fit_save_word_sequence():
  116. word_to_sequence = Word2Sequence()
  117. train_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
  118. total_file_path_list = []
  119. for i in train_path:
  120. total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
  121. for cur_path in tqdm(total_file_path_list, ascii=True, desc="fitting"):
  122. word_to_sequence.fit(tokenize(open(cur_path, encoding="utf-8").read().strip()))
  123. word_to_sequence.build_vocab()
  124. pickle.dump(word_to_sequence, open("model/ws.pkl", "wb"))
  125. ws = pickle.load(open("./model/ws.pkl", "rb"))
  126. # print(len(ws))
  127. # 模型
  128. class IMDBModel(nn.Module):
  129. def __init__(self):
  130. super(IMDBModel, self).__init__()
  131. self.hidden_size = 64
  132. self.embedding_dim = 200
  133. self.num_layer = 2
  134. self.bidirectional = True
  135. self.bi_num = 2 if self.bidirectional else 1
  136. self.dropout = 0.5
  137. # 以上部分为超参数,可以自行修改
  138. self.embedding = nn.Embedding(len(ws), self.embedding_dim, padding_idx=ws.PAD)
  139. self.lstm = nn.LSTM(self.embedding_dim, self.hidden_size,
  140. self.num_layer, bidirectional=True, dropout=self.dropout)
  141. self.fc = nn.Linear(self.hidden_size * self.bi_num, 20)
  142. self.fc2 = nn.Linear(20, 10)
  143. def forward(self, x):
  144. x = self.embedding(x)
  145. x = x.permute(1, 0, 2) # 进行轴交换
  146. h_0, c_0 = self.init_hidden_state(x.size(1))
  147. _, (h_n, c_n) = self.lstm(x, (h_0, c_0))
  148. # 只要最后一个lstm单元处理的结果,取前向LSTM和后向LSTM的结果进行简单拼接
  149. out = torch.cat([h_n[-2, :, :], h_n[-1, :, :]], dim=-1)
  150. out = self.fc(out)
  151. out = F.relu(out)
  152. out = self.fc2(out)
  153. return F.log_softmax(out, dim=-1)
  154. def init_hidden_state(self, batch_size):
  155. h_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
  156. c_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
  157. return h_0, c_0
  158. imdb_model = IMDBModel()
  159. optimizer = optim.Adam(imdb_model.parameters())
  160. criterion = nn.CrossEntropyLoss()
  161. def train(epoch):
  162. mode = True
  163. train_dataloader = get_dataloader(mode)
  164. for idx, (target, input) in enumerate(train_dataloader):
  165. optimizer.zero_grad()
  166. output = imdb_model(input)
  167. loss = F.nll_loss(output, target)
  168. loss.backward()
  169. optimizer.step()
  170. if idx % 10 == 0:
  171. print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
  172. epoch, idx * len(input), len(train_dataloader.dataset),
  173. 100. * idx / len(train_dataloader), loss.item()))
  174. torch.save(imdb_model.state_dict(), "model/mnist_net_lstm.pkl")
  175. torch.save(optimizer.state_dict(), 'model/mnist_optimizer_lstm.pkl')
  176. def test():
  177. test_loss = 0
  178. correct = 0
  179. mode = False
  180. imdb_model.eval()
  181. test_dataloader = get_dataloader(mode)
  182. with torch.no_grad():
  183. for target, input in test_dataloader:
  184. output = imdb_model(input)
  185. test_loss += F.nll_loss(output, target, reduction="sum")
  186. pred = torch.max(output, dim=-1, keepdim=False)[-1]
  187. correct += pred.eq(target.data).sum()
  188. test_loss = test_loss / len(test_dataloader.dataset)
  189. print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
  190. test_loss, correct, len(test_dataloader.dataset),
  191. 100. * correct / len(test_dataloader.dataset)))
  192. if __name__ == '__main__':
  193. # # 测试数据集的功能
  194. # dataset = ImdbDataset(mode="train")
  195. # dataloader = DataLoader(dataset=dataset, batch_size=2, shuffle=True, collate_fn=collate_fn)
  196. # for idx, (label, text) in enumerate(dataloader):
  197. # print("idx:", idx)
  198. # print("lable:", label)
  199. # print("text:", text)
  200. # break
  201. # 测试Word2Sequence
  202. # fit_save_word_sequence()
  203. # w2s = Word2Sequence()
  204. # voc = [["你", "好", "么"],
  205. # ["你", "好", "哦"]]
  206. # for i in voc:
  207. # w2s.fit(i)
  208. # w2s.build_vocab()
  209. # print(w2s.dict)
  210. # print(w2s.transform(["你", "好", "嘛"]))
  211. # 训练和测试
  212. test()
  213. for i in range(3):
  214. train(i)
  215. print(
  216. "训练第{}轮的测试结果-----------------------------------------------------------------------------------------".format(
  217. i + 1))
  218. test()

5.3 测试结果

以下是在我的电脑中,仅仅只使用CPU的情况下跑得的结果,训练和测试都比较慢,其中LSTM的效果不理想,可能是数据集不足以支撑起10分类。

1.没有训练,直接测试:精确率大约在15%左右

2.全连接神经网络:精确率大约在25%左右

3.LSTM:精确率大约在35%左右

结果仅供参考

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/755532
推荐阅读
相关标签
  

闽ICP备14008679号