使用pytorch和torchtext进行文本分类的实例


Posted in Python onJanuary 08, 2020

文本分类是NLP领域的较为容易的入门问题,本文记录我自己在做文本分类任务以及复现相关论文时的基本流程,绝大部分操作都使用了torch和torchtext两个库。

1. 文本数据预处理

首先数据存储在三个csv文件中,分别是train.csv,valid.csv,test.csv,第一列存储的是文本数据,例如情感分类问题经常是用户的评论review,例如imdb或者amazon数据集。第二列是情感极性polarity,N分类问题的话就有N个值,假设值得范围是0~N-1。

下面是很常见的文本预处理流程,英文文本的话不需要分词,直接按空格split就行了,这里只会主要说说第4点。

1、去除非文本部分

2、分词

3、去除停用词

4、对英文单词进行词干提取(stemming)和词型还原(lemmatization)

5、转为小写

6、特征处理

Bag of Words

Tf-idf

N-gram

Word2vec

词干提取和词型还原

from nltk.stem import SnowballStemmer
stemmer = SnowballStemmer("english") # 选择语言
from nltk.stem import WordNetLemmatizer 
wnl = WordNetLemmatizer()

SnowballStemmer较为激进,转换有可能出现错误,这里较为推荐使用WordNetLemmatizer,它一般只在非常肯定的情况下才进行转换,否则会返回原来的单词。

stemmer.stem('knives')
# knive
wnl.lemmatize('knives')
# knife

因为我没有系统学习和研究过NLTK的代码,所以就不多说了,有兴趣的可以自己去阅读NLTK的源码。

2. 使用torchtext加载文本数据

本节主要是用的模块是torchtext里的data模块,处理的数据同上一节所描述。

首先定义一个tokenizer用来处理文本,比如分词,小写化,如果你已经根据上一节的词干提取和词型还原的方法处理过文本里的每一个单词后可以直接分词就够了。

tokenize = lambda x: x.split()

或者也可以更保险点,使用spacy库,不过就肯定更耗费时间了。

import spacy

spacy_en = spacy.load('en')
def tokenizer(text):
 return [toke.text for toke in spacy_en.tokenizer(text)]

然后要定义Field,至于Field是啥,你可以简单地把它理解为一个能够加载、预处理和存储文本数据和标签的对象。我们可以用它根据训练数据来建立词表,加载预训练的Glove词向量等等。

def DataLoader():
 tokenize = lambda x: x.split()
 # 用户评论,include_lengths设为True是为了方便之后使用torch的pack_padded_sequence
 REVIEW = data.Field(sequential=True,tokenize=tokenize, include_lengths=True)
 # 情感极性
 POLARITY = data.LabelField(sequential=False, use_vocab=False, dtype = torch.long)
 # 假如train.csv文件并不是只有两列,比如1、3列是review和polarity,2列是我们不需要的数据,
 # 那么就要添加一个全是None的元组, fields列表存储的Field的顺序必须和csv文件中每一列的顺序对应,
 # 否则review可能就加载到polarity Field里去了
 fields = [('review', REVIEW), (None, None), ('polarity', POLARITY)]
 
 # 加载train,valid,test数据
 train_data, valid_data, test_data = data.TabularDataset.splits(
         path = 'amazon',
         train = 'train.csv',
      validation = 'valid.csv',
      test = 'test.csv',
         format = 'csv',
         fields = fields,
         skip_header = False # 是否跳过文件的第一行
 )
 return REVIEW, POLARITY, train_data

加载完数据可以开始建词表。如果本地没有预训练的词向量文件,在运行下面的代码时会自动下载到当前文件夹下的'.vector_cache'文件夹内,如果本地已经下好了,可以用Vectors指定文件名name,路径cache,还可以使用Glove。

from torchtext.vocab import Vectors, Glove
import torch

REVIEW, POLARITY, train_data = DataLoader()
# vectors = Vectors(name='glove.6B.300d.txt', cache='.vector_cache')
REVIEW.build_vocab(train_data, # 建词表是用训练集建,不要用验证集和测试集
     max_size=400000, # 单词表容量
     vectors='glove.6B.300d', # 还有'glove.840B.300d'已经很多可以选
     unk_init=torch.Tensor.normal_ # 初始化train_data中不存在预训练词向量词表中的单词
)

# print(REVIEW.vocab.freqs.most_common(20)) 数据集里最常出现的20个单词
# print(REVIEW.vocab.itos[:10])  列表 index to word
# print(REVIEW.vocab.stoi)    字典 word to index

接着就是把预训练词向量加载到model的embedding weight里去了。

pretrained_embeddings = REVIEW.vocab.vectors
model.embedding.weight.data.copy_(pretrained_embeddings)
UNK_IDX = REVIEW.vocab.stoi[REVIEW.unk_token]
PAD_IDX = REVIEW.vocab.stoi[REVIEW.pad_token]
# 因为预训练的权重的unk和pad的词向量不是在我们的数据集语料上训练得到的,所以最好置零
model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)

然后用torchtext的迭代器来批量加载数据,torchtext.data里的BucketIterator非常好用,它可以把长度相近的文本数据尽量都放到一个batch里,这样最大程度地减少padding,数据就少了很多无意义的0,也减少了矩阵计算量,也许还能对最终准确度有帮助(误)?我凭直觉猜的,没有做实验对比过,但是至少能加速训练迭代应该是没有疑问的,如果哪天我有钱了买了台好点的服务器做完实验再来补充。

sort_within_batch设为True的话,一个batch内的数据就会按sort_key的排列规则降序排列,sort_key是排列的规则,这里使用的是review的长度,即每条用户评论所包含的单词数量。

train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
            (train_data, valid_data, test_data),
            batch_size=32,
            sort_within_batch=True,
            sort_key = lambda x:len(x.review),
            device=torch.device('cpu'))

最后就是加载数据喂给模型了。

for batch in train_iterator:
 # 因为REVIEW Field的inclue_lengths为True,所以还会包含一个句子长度的Tensor
 review, review_len = batch.review 
 # review.size = (seq_length, batch_size) , review_len.size = (batch_size, )
 polarity = batch.polarity
 # polarity.size = (batch_size, )
 predictions = model(review, review_lengths)
 loss = criterion(predictions, polarity) # criterion = nn.CrossEntropyLoss()

3. 使用pytorch写一个LSTM情感分类器

下面是我简略写的一个模型,仅供参考

import torch.nn as nn
import torch.nn.functional as F
from torch.nn.utils.rnn import pack_padded_sequence
import torch


class LSTM(nn.Module):

 def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim,
     n_layers, bidirectional, dropout, pad_idx):
  super(LSTM, self).__init__()
  self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
  self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=n_layers,
       bidirectional=bidirectional, dropout=dropout)
  self.Ws = nn.Parameter(torch.Tensor(hidden_dim, output_dim))
  self.bs = nn.Parameter(torch.zeros((output_dim, )))
  nn.init.uniform_(self.Ws, -0.1, 0.1)
  nn.init.uniform_(self.bs, -0.1, 0.1)
  self.dropout = nn.Dropout(p=0.5)

 def forward(self, x, x_len):
  x = self.embedding(x)
  x = pack_padded_sequence(x, x_len)
  H, (h_n, c_n) = self.lstm(x)
  h_n = self.dropout(h_n)
  h_n = torch.squeeze(h_n)
  res = torch.matmul(h_n, self.Ws) + self.bs
  y = F.softmax(res, dim=1)
  # y.size(batch_size, output_dim)
  return y

训练函数

def train(model, iterator, optimizer, criterion):
 epoch_loss = 0
 num_sample = 0
 correct = 0

 model.train()
 for batch in iterator:
  optimizer.zero_grad()
  review, review_lengths = batch.review
  polarity = batch.polarity
  predictions = model(review, review_lengths)
  correct += torch.sum(torch.argmax(preds, dim=1) == polarity)
  loss = criterion(predictions, polarity)
  loss.backward()
  epoch_loss += loss.item()
  num_sample += len(batch)
  optimizer.step()

 return epoch_loss / num_sample, correct.float() / num_sample

if __name__ == '__main__':
 for epoch in range(N_EPOCHS):
 train_loss, acc = train(model, train_iter, optimizer, criterion)
 print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {acc* 100:.2f}%')

注意事项和遇到的一些坑

文本情感分类需不需要去除停用词?

应该是不用的,否则acc有可能下降。

data.TabularDataset.splits虽然好用,但是如果你只想加载训练集,这时候如果直接不给validation和test参数赋值,那么其他代码和原来一样,比如这样

train_data = data.TabularDataset.splits(
         path = '',
         train = 'train.csv',
         format = 'csv',
         fields = fields,
         skip_header = False # 是否跳过文件的第一行
)

那么底下你一定会报错,因为data.TabularDataset.splits返回的是一个元组,也就是如果是训练验证测试三个文件都给了函数,就返回(train_data, valid_data, test_data),这时候你用三个变量去接受函数返回值当然没问题,元组会自动拆包。

当只给函数一个文件train.csv时,函数返回的是(train_data)而非train_data,因此正确的写法应该如下

train_data = data.TabularDataset.splits(
         path = '',
         train = 'train.csv',
         format = 'csv',
         fields = fields,
         skip_header = False # 是否跳过文件的第一行
)[0] # 注意这里的切片,选择元组的第一个也是唯一一个元素赋给train_data

同理data.BucketIterator.splits也有相同的问题,它不但返回的是元组,它的参数datasets要求也是以元组形式,即(train_data, valid_data, test_data)进行赋值,否则在下面的运行中也会出现各种各样奇怪的问题。

如果你要生成两个及以上的迭代器,那么没问题,直接照上面写就完事了。

如果你只要生成train_iterator,那么正确的写法应该是下面这样

train_iter = data.BucketIterator(
   train_data,
   batch_size=32,
   sort_key=lambda x:len(x.review),
   sort_within_batch=True,
   shuffle=True # 训练集需要shuffle,但因为验证测试集不需要
    # 可以生成验证和测试集的迭代器直接用data.iterator.Iterator类就足够了
)

出现的问题 x = pack_padded_sequence(x, x_len) 当数据集有长度为0的句子时, 就会后面报错

Adagrad效果比Adam好的多

4. 总结

不仅仅是NLP领域,在各大顶会中,越来越多的学者选择使用Pytorch而非TensorFlow,主要原因就是因为它的易用性,torchtext和pytorch搭配起来是非常方便的NLP工具,可以大大缩短文本预处理,加载数据的时间。

我本人之前用过tf 1.x以及keras,最终拥抱了Pytorch,也是因为它与Numpy极其类似的用法,更Pythonic的代码,清晰的源码让我在遇到bug时能一步一步找到问题所在,动态图让人能随时看到输出的Tensor的全部信息,这些都是Pytorch的优势。

现在tf 2.0也在不断改进,有人笑称tf越来越像pytorch了,其实pytorch也在不断向tf学习,在工业界,tf仍然处于王者地位,不知道未来pytorch能不能在工业界也与tf平分秋色,甚至更胜一筹呢?

以上这篇使用pytorch和torchtext进行文本分类的实例就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
web.py在SAE中的Session问题解决方法(使用mysql存储)
Jun 24 Python
Python中列表和元组的使用方法和区别详解
Dec 30 Python
你所不知道的Python奇技淫巧13招【实用】
Dec 14 Python
python 统计代码行数简单实例
May 04 Python
Python设计模式之观察者模式简单示例
Jan 10 Python
Python实现重建二叉树的三种方法详解
Jun 23 Python
用于业余项目的8个优秀Python库
Sep 21 Python
Python 确定多项式拟合/回归的阶数实例
Dec 29 Python
基于python生成器封装的协程类
Mar 20 Python
python实现图片压缩代码实例
Aug 12 Python
python MultipartEncoder传输zip文件实例
Apr 07 Python
Python 图片处理库exifread详解
Feb 25 Python
python爬虫爬取监控教务系统的思路详解
Jan 08 #Python
Pytorch实现基于CharRNN的文本分类与生成示例
Jan 08 #Python
python实现单目标、多目标、多尺度、自定义特征的KCF跟踪算法(实例代码)
Jan 08 #Python
Pytorch实现神经网络的分类方式
Jan 08 #Python
python 爬取古诗文存入mysql数据库的方法
Jan 08 #Python
基于python3抓取pinpoint应用信息入库
Jan 08 #Python
Python PyInstaller安装和使用教程详解
Jan 08 #Python
You might like
关于文本留言本的分页代码
2006/10/09 PHP
Mysql中limit的用法方法详解与注意事项
2008/04/19 PHP
php XPath对XML文件查找及修改实现代码
2011/07/27 PHP
解析smarty模板中类似for的功能实现
2013/06/18 PHP
destoon实现不同会员组公司名称显示不同的颜色的方法
2014/08/22 PHP
PHP数组函数array_multisort()用法实例分析
2016/04/02 PHP
Laravel5.1 框架路由基础详解
2020/01/04 PHP
innertext , insertadjacentelement , insertadjacenthtml , insertadjacenttext 等区别
2007/06/29 Javascript
javascript fullscreen全屏实现代码
2009/04/09 Javascript
基于JQuery的数字改变的动画效果--可用来做计数器
2010/08/11 Javascript
javascript中运用闭包和自执行函数解决大量的全局变量问题
2010/12/30 Javascript
在JavaScript中监听IME键盘输入事件
2011/05/29 Javascript
深入document.write()与HTML4.01的非成对标签的详解
2013/05/08 Javascript
JS事件在IE与FF中的区别详细解析
2013/11/20 Javascript
JS网页图片按比例自适应缩放实现方法
2014/01/15 Javascript
解决自定义$(id)的方法与jquery选择器$冲突的问题
2014/06/14 Javascript
jquery实现全选、反选、获得所有选中的checkbox
2020/09/13 Javascript
js 上传文件预览的简单实例
2016/08/16 Javascript
详解jQuery中的事件
2016/12/14 Javascript
详解webpack 入门总结和实践(按需异步加载,css单独打包,生成多个入口文件)
2017/06/20 Javascript
基于JS实现仿京东搜索栏随滑动透明度渐变效果
2017/07/10 Javascript
解决vue中post方式提交数据后台无法接收的问题
2018/08/11 Javascript
axios对请求各种异常情况处理的封装方法
2018/09/25 Javascript
element-ui table行点击获取行索引(index)并利用索引更换行顺序
2020/02/27 Javascript
[01:04]DOTA2:伟大的Roshan雕塑震撼来临
2015/01/30 DOTA
Python正确重载运算符的方法示例详解
2017/08/27 Python
python利用requests库模拟post请求时json的使用教程
2018/12/07 Python
Python中logging日志的四个等级和使用
2020/11/17 Python
纯css3实现的动画按钮的实例教程
2014/11/17 HTML / CSS
中国专业的综合网上购物商城:京东
2016/08/02 全球购物
悬挂训练绳:TRX
2017/12/14 全球购物
医学类导师推荐信范文
2013/11/19 职场文书
4S店售后客服自我评价
2014/04/09 职场文书
研究生简历自我评
2015/03/11 职场文书
2015年大学生社会实践评语
2015/03/26 职场文书
Python中threading库实现线程锁与释放锁
2021/05/17 Python