使用pytorch做文本多分类的原理与代码保姆级详解

在B站看到刘洪普老师的视频,用RNN对18个国家的人名进行多分类的训练,然后对人名所属的国家进行预测的实战。这里,我将视频中的代码和思路进行了消化和总结,在这里和大家一起分享一下。
首先就是看一下数据集,我们可以读取一下数据进行观察:

import gzip
import csv
import random
res =[]
is_train_set = True
file_name = 'D:/BaiduNetdiskDownload/PyTorch深度学习实践/names_train.csv.gz' if is_train_set else 'D:/BaiduNetdiskDownload/PyTorch深度学习实践/names_test.csv.gz'
with gzip.open(file_name, 'rt') as f:
    reader = csv.reader(f)
    rows = list(reader)
for _ in range(30):
    print(rows[random.randint(1, len(rows)-1)])

在这里我从所有的训练数据中随机抽取30个进行查看,可以得到如下结果:
数据样本

随机抽取的30个训练集样本如图所示,有很多不同的名字,对应着不同的国家。看到训练集,我们也可以大致了解到我们的任务就是输入一个人名,输出这个人名所对应的国家。我们遍历所有的数据,然后将所有的国家做成一个集合,可以看到有18个国家,每个国家都有对应的标号,下面就是我提取的18个国家的名称:

{'Arabic': 0, 'Chinese': 1, 'Czech': 2, 'Dutch': 3, 'English': 4, 'French': 5, 'German': 6, 'Greek': 7, 'Irish': 8,
 'Italian': 9, 'Japanese': 10, 'Korean': 11, 'Polish': 12, 'Portuguese': 13, 'Russian': 14, 'Scottish': 15, 
 'Spanish': 16, 'Vietnamese': 17}

在我们大致了解了整个项目之后,我们开始来看看如何来进行深度神经网络的设计,首先在自然语言处理中,RNN是处理序列最常用到的网络,就像CNN在处理图像中有着不可被取代的地位。那么我们来看看如何运用RNN来进行该任务的架构设计:
任务架构
从该任务架构图中可以看出,我们输入的序列 x 1 , x 2 , x 3 . . . x n {x_1, x_2, x_3...x_n} x1,x2,x3...xn 经过嵌入层之后,我们将序列向量化之后,依次放到RNN层然后序列迭代完毕之后,我们可以得到一个中间向量 h n h_n hn,我们将 h n h_n hn 经过线性层展开之后,再经过一层 softmax,就可以输出结果,我们去输出向量中最大的那一项,就是我们要的结果。
那么代码如何实现这个任务呢?首先我们要将数据进行预处理,这也是整个任务中最重要也是最麻烦的一步了。首先,我们输入是一个序列,那么我们如何将这个序列进行向量化?首先我们就要建立词典,然后对序列进行映射操作,这里我们的序列是人名中的字符,我们可以直接使用ASCII来对序列进行映射,下表就是对序列进行映射操作的示意图:

NameCharactersASCIIPadding
Maclean[‘M’,‘a’,‘c’,‘l’,‘e’,‘a’,‘n’][77 97 99 108 101 97 110][77 97 99 108 101 97 110 0 0 0]
Usami[‘U’,‘s’,‘a’,‘m’,‘i’][85 115 97 109 105][85 115 97 109 105 0 0 0 0 0]
Nasikovsky[‘N’,‘a’,‘s’,‘i’,‘k’,‘o’,‘v’,‘s’,‘k’,‘y’][78 97 115 105 107 111 118 115 107 121][78 97 115 105 107 111 118 115 107 121]
Balagul[‘B’,‘a’,‘l’,‘a’,‘g’,‘u’,‘l’][66 97 108 97 103 117 108][66 97 108 97 103 117 108 0 0 0]
Tansho[‘T’,‘a’,‘n’,‘s’,‘h’,‘o’][84 97 110 115 104 111][84 97 110 115 104 111 0 0 0 0]

由这张表可以得知,我们通过ASCII表来将人名拆成一个个的字符,然后进行ASCII映射,因为我们再喂数据的时候需要shape统一,所以我们还需要进行padding的操作,需要将最长的序列作为维度的长度,然后其他不够此长度的补0处理,从上表可以很清晰地理解这个操作。下面就是准备数据集的代码:

class NameDataset(Dataset):
    def __init__(self, is_train_set=True):
    	# 指定训练集和测试集
        file_name = 'data/names_train.csv.gz' if is_train_set else 'data/names_test.csv.gz'
        with gzip.open(file_name, 'rt') as f:
            reader = csv.reader(f)
            rows = list(reader)
        # 人名
        self.names = [row[0] for row in rows]
        # 人名序列的长度
        self.length = len(self.names)
        # 人名所对应的国家
        self.countries = [row[1] for row in rows]
        # 所有国家的集合
        self.country_list = list(sorted(set(self.countries)))
        # 国家和index生成的字典
        self.country_dict = self.getCountryDict()
        # 国家的数量
        self.country_num = len(self.country_list)

    def __getitem__(self, index):
    	# 返回人名和所对应的国家名
        return self.names[index], self.country_dict[self.countries[index]]

    def __len__(self):
    	# 返回人名的长度
        return self.length

    def getCountryDict(self):
        country_dict = {}
        # 遍历数据建立国家的字典
        for idx, country_name in enumerate(self.country_list, 0):
            country_dict[country_name] = idx
        return country_dict

    def idx2country(self, index):
    	# 将index转换成国家名
        return self.country_list[index]

    def getCountryNum(self):
    	# 获取不同国家的数量
        return self.country_num

这就定义了数据集的类,代码中有详细的注释,然后我们需要对输入的数据集进行准备,代码如下所示:

# 建立训练集的dataloader
train_set = NameDataset(is_train_set=True)
trainloader = DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True)
# 建立测试集的dataloader
test_set = NameDataset(is_train_set=False)
testloader = DataLoader(test_set, batch_size=BATCH_SIZE, shuffle=True)
# 获取国家数
N_COUNTRY = train_set.getCountryNum()

在这里我们可以先指定一下,我们训练的一些基本的参数:

# 隐藏层的维度
HIDDEN_SIZE = 100
BATCH_SIZE = 256
# RNN的层数
N_LAYERS = 2
# 训练的轮数,暂定500轮
N_EPOCHS = 500
# 字符长度,也就是输入的维度
N_CHARS = 128
# 是否使用GPU
USE_GPU = False

参数指定完成之后,我们来看看详细的整体任务架构,一般来说单层的RNN只能从前往后地遍历序列,所以只能捕捉序列从前往后的序列信息,这里我们用双层RNN,第二层从后往前遍历,获取序列从后往前的信息,整体的架构图如下所示:
双层RNN架构
上图就是双向的RNN的任务架构图,在图中我们可以看到有两层的RNN层,分别是正向和逆向的,然后将正向和逆向的中间变量进行concat,就可以得到输出序列,hidden层是 h i d d e n = [ h f n , h b n ] hidden = [h_f^n, h_b^n] hidden=[hfn,hbn]。根据这张双向RNN的架构图,我们可以设计这个架构图的代码,如下所示:

class RNNClassifier(torch.nn.Module):
    def __init__(self, input_size, hidden_size, output_size, n_layers=1, bidirectional=True):
        super(RNNClassifier, self).__init__()
        # RNN隐藏层的维度
        self.hidden_size = hidden_size
        # 有多少层RNN
        self.n_layers = n_layers
        # 是否使用双向RNN
        self.n_directions = 2 if bidirectional else 1
        # 将序列进行embedding操作,维度为(seq_length(input_size), batch_size, hidden_size),input_size为字典的大小
        self.embedding = torch.nn.Embedding(input_size, hidden_size)
        # 这里RNN我们使用GRU,输入维度是embedding层的输出维度hidden_size,输出维度也为hidden_size,
        # 整个GRU的输入维度是(seq_length(input_size), batch_size, hidden_size)
        # hidden 的维度是(n_layers * nDirectional, batch_size, hidden_size)
        # 输出的维度是(seq_length, batch_size, hidden_size*nDirectional(双向concat))
        self.gru = torch.nn.GRU(input_size=hidden_size, hidden_size=hidden_size, num_layers=n_layers,
                                bidirectional=bidirectional)
        # 最后一层线性层,输入为hidden_size * self.n_directions,输出为output_size国家数
        self.fc = torch.nn.Linear(hidden_size * self.n_directions, output_size)

    def _init_hidden(self, batch_szie):
    	# hidden 的维度是(n_layers * nDirectional, batch_size, hidden_size)
        hidden = torch.zeros(self.n_layers * self.n_directions, batch_szie, self.hidden_size)
        # 返回hidden的向量
        return create_tensor(hidden)

    def forward(self, input, seq_len):
        # batch * seq -> seq * batch 转置
        input = input.t()
        # 提取input第一个维度batch_size
        batch_size = input.size(1)
		# 初始化hidden的向量,(n_layers*n_Direction, batch_size, hidden_size)
        hidden = self._init_hidden(batch_szie=batch_size)
        # embedding层,(seq_length, batch_size, hiddensize)
        embedding = self.embedding(input=input)

        # pack them up,打包序列,将序列中非零元素的向量进行拼接,使得GRU单元可以处理长短不一的序列。
        # 需要将输入序列进行降序排序,并记录每个batch的长度。形成seq_length的数组
        gru_input = pack_padded_sequence(embedding, seq_len)
		# 通过GRU层之后的中间变量hidden,和输出output,不懂的可以看看GRU源码
        output, hidden = self.gru(gru_input, hidden)
        # 如果是双向GRU,那么就将前向和反向hidden向量进行concat
        if self.n_directions == 2:
            hidden_cat = torch.cat([hidden[-1], hidden[-1]], dim=1)
        else:
            hidden_cat = hidden[-1]
        # 最后经过一层全连接层
        fc_output = self.fc(hidden_cat)
        # 返回全连接层之后的结果
        return fc_output

之间的处理过程可能很难理解,我们通过这个图来表示一下:
序列变换
我们原始的数据横向是batch_size,纵向是seq_length,由于我们要做pack_padded_sequence处理,所以我们要将序列进行降序处理,并且记录我们序列的seq_length,这一块的代码如下所示:

# 创建训练所需要的张量方法
def make_tensor(names, countries):
	# 通过下面的方法,将名字字符串转换成序列,返回序列(len(arr(name))*len(names))以及序列的长度序列
    sequences_and_lengths = [name2list(name=name) for name in names]
    # 名字的字符串序列
    name_sequences = [s1[0] for s1 in sequences_and_lengths]
    # 名字的序列长度所构成的序列
    seq_lengths = torch.LongTensor([s1[1] for s1 in sequences_and_lengths])
    # 把国家的int型转成long Tensor
    countries = countries.long()
    # make tensor of name, batch * seq_len
    # 先将batch_size * seq_length 填成0向量的Tensor
    seq_tensor = torch.zeros(len(name_sequences), seq_lengths.max()).long()
    # 然后我们在将名字序列以及seq_length序列填充值到该张量中去
    for idx, (seq, seq_len) in enumerate(zip(name_sequences, seq_lengths), 0):
        seq_tensor[idx, :seq_len] = torch.LongTensor(seq)
    # sort by length to use pack_padded_seq,像上图中那样进行降序排列,排序依据是seq_length长度,得到新的seq_length以及索引
    seq_lengths, perm_idx = seq_lengths.sort(dim=0, descending=True)
    seq_tensor = seq_tensor[perm_idx]
    countries = countries[perm_idx]
    # 返回序列的tensor,序列长度的tensor以及对应国家的tensor
    return create_tensor(seq_tensor), create_tensor(seq_lengths), create_tensor(countries)

# 判断是否使用GPU的方法
def create_tensor(tensor):
    if USE_GPU:
        device = torch.device('cuda:0')
        tensor = tensor.to(device)
    return tensor

# 将所有的名字string转换成ASCII列表
def name2list(name):
	# 将名字转换成ASCII标中对应的数字,并返回序列,以及序列长度
    arr = [ord(c) for c in name]
    return arr, len(arr)

运用上面的方法,我们数据预处理就全部搞定了,整个的流程用示意图可以表示成如下图所示:
在这里插入图片描述

这张图可以清晰地表示我们数据预处理的整体过程,可以说整个流程在上图中已经体现得淋漓尽致了。万事俱备之后,我们就需要进行训练模型的操作,接下来我们就需要进行的代码编写:

def trainModel():
	# 定义总的损失
    total_loss = 0
    for i, (names, countries) in enumerate(trainloader, 1):
    	# 通过数据集创建输入,seq_length和标签的tensor
        inputs, seq_lengths, target = make_tensor(names, countries)
        # 定义模型的输出
        output = classifier(inputs, seq_lengths)
        # 比较输出与真实标签的loss
        loss = criterion(output, target)
        # 反向传播,更新权重
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
		# 更新损失
        total_loss += loss.item()
        if i % 10 == 0:
            print(f'[{i * len(inputs)}/{len(train_set)}]', end='')
            print(f'loss={total_loss / (i * len(inputs))}')
        # 返回训练过程中的损失
        return total_loss

在训练方法中需要定义损失函数loss的方法,以及梯度下降optimizer的方法,这里由于是一个多分类的任务,我们需要用的是交叉熵损失函数,梯度下降的方法使用动量和自适应学习率来加快收敛速度的Adam。定义的代码如下所示:

# 实例化模型classifier
classifier = RNNClassifier(N_CHARS, HIDDEN_SIZE, N_COUNTRY, N_LAYERS)
# if USE_GPU:
#     device = torch.device('cuda:0')
#     classifier.to(device)
# 定义损失函数criterion,使用交叉熵损失函数
criterion = torch.nn.CrossEntropyLoss()
# 梯度下降使用的Adam算法
optimizer = torch.optim.Adam(classifier.parameters(), lr=0.001)

有了训练的方法,我们再来定义测试的方法,测试的方法也是通过准确度的计算,来评判模型的好坏,代码如下:

def testModel():
	# 预测准确的个数
    correct = 0
    # 测试集的大小
    total = len(test_set)
    print('evaluating trained model...')
    with torch.no_grad():
        for i, (names, countries) in enumerate(testloader, 1):
        	# 定义相关的tensor
            inputs, seq_lengths, target = make_tensor(names, countries)
            # 模型的输出
            output = classifier(inputs, seq_lengths)
            # 取线性层输出最大的一个作为分类的结果
            pred = output.max(dim=1, keepdim=True)[1]
            # 正确预测个数的累加
            correct += pred.eq(target.view_as(pred)).sum().item()
        # 计算准确率
        percent = '%.2f' % (100 * correct / total)
        print(f'test set: accuracy {correct}/{total}\n{percent}%')
    # 返回模型测试集的准确率
    return correct / total

写好了训练方法与测试的方法之后,我们可以开始进行模型训练与测试了,代码如下:

print('training for %d epochs.' % N_EPOCHS)
acc_list = []
for epoch in range(1, N_EPOCHS + 1):
	trainModel()
	acc = testModel()
	acc_list.append(acc)
	print('acc_list: ', acc_list)

我们得到训练模型在测试集的准确率的曲线图,一共500轮训练,准确率的图像如下所示:
准确率曲线
从这个准确率的图像清楚地表现出模型在训练中的效果,在300轮迭代之后,模型的效果趋近于平稳,模型在测试集上大概有85%左右的准确率,鉴于这是一个18类别的多分类任务以及训练集的局限性,85%的准确率还是不错的。模型训练好之后,我们可以将模型进行保存,代码如下所示:

torch.save(classifier.state_dict(), 'name_classifier_model.pt')

模型存储之后,我们可以通过load方法对模型进行调用,调出模型的代码如下:

classifier.load_state_dict(torch.load('name_classifier_model.pt'))

模型导出之后,我们如何来直观地对名字进行预测呢?这里我们就需要写一个预测的方法,将名字进行向量化,然后通过模型进行预测,下面就是预测名字的方法:

def predict_country(name):
	# 同上,名字序列和长度,这里长度为1,因为输入的是单一的名字
    sequences_and_lengths = [name2list(name=name)]
    # 名字的序列映射
    name_sequences = [sequences_and_lengths[0][0]]
    # 序列长度的张量
    seq_lengths = torch.LongTensor([sequences_and_lengths[0][1]])
    print('sequences_and_lengths: ', sequences_and_lengths)
	# 创建序列的张量
    seq_tensor = torch.zeros(len(name_sequences), seq_lengths.max()).long()
    for idx, (seq, seq_len) in enumerate(zip(name_sequences, seq_lengths), 0):
        seq_tensor[idx, :seq_len] = torch.LongTensor(seq)
    #名字的张量
    inputs = create_tensor(seq_tensor)
    # seq_length的张量
    seq_lengths = create_tensor(seq_lengths)
    # 通过模型进行预测输出output张量
    output = classifier(inputs, seq_lengths)
    # 通过线性层的输出取最大项作为预测项输出
    pred = output.max(dim=1, keepdim=True)[1]
    # 返回预测的index
    return pred.item()

预测的方法完成之后,激动人心的时刻来临了,我们可以试试效果如何,我随机选取了几个名字来预测:
首先是日本的本田,英文是:Honda
Honda
然后是日本的乒乓球选手石川佳纯,英文:Ishikawa
Ishikawa
然后我把我的姓氏陈,英文:Chen
Chen
然后是懂王川建国,英文:Trump
Trump
在这里英美的名字都归于English一类,所以懂王的名字预测是没问题的。
然后就是韩国的大姓金,英文:Kim
Kim
然后是大帝普京,英文:PutinPutin
随便测了几个,效果还是很不错的,感觉是一个很有趣的任务,大家也可以动手弄一套试试,很有意思,希望这篇博文能够帮助大家理解pytorch来进行文本多分类的任务。每一行代码我都做了注释,可以说是保姆级的讲解,希望能够对大家有所帮助。因本人能力有限,文中如有纰漏,也希望大家不吝指教;如有转载,也请注明出处,谢谢大家。

  • 5
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
针对pytorch transform的文本分类模型推理,需要经过以下几个步骤。 首先,利用`torchtext`库中的`Field`类和`TabularDataset`类对文本数据进行预处理和加载。在`Field`类中,可以指定对文本进行分词、形成词表的方式,同时也可以指定标签和文本数据的字段。在`TabularDataset`类中,可以将已处理的文本数据根据标签和文本字段进行打包,以便后续使用。 然后,需要将`TabularDataset`类加载的文本数据转化成可用于模型推理的数据格式。这个过程可以利用`BucketIterator`类进行快捷处理,该类可以将文本数据自动分batch,同时对每个batch中的文本进行padding以保证长度一致。 接着,需要加载预训练模型、将模型移到GPU或CPU上,并通过`eval()`方法将模型设定为推理模式。在推理模式下,模型会关闭Dropout等随机性操作。 对于每个输入的文本,在进行预测之前需要将其转化成模型需要的数据形式。可以利用词表将文本转化成对应的整数序列,然后使用`torch.LongTensor()`将其转化成可用于模型输入的数据类型。 最后,将处理好的文本数据传入模型进行推理,并得到模型对每个文本的标签预测。在这个过程中,可以通过`with torch.no_grad()`语句关闭梯度计算,减少计算量和内存占用。 以上是使用pytorch transform进行文本分类模型推理的基本步骤。通过合理调整文本处理和模型参数,可以得到更好的性能和效果。
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值