Pytorch中文文本分类
本文为🔗365天深度学习训练营内部文章
原作者:K同学啊
将对中文文本进行分类,示例如下:
文本分类流程图
1.加载数据
import time
import pandas as pd
import torch
from torch.utils.data import DataLoader, random_split
import torch.nn as nn
import torchvision
from torchtext.data import to_map_style_dataset
from torchvision import transforms,datasets
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
import jieba
import warningswarnings.filterwarnings('ignore')
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")'''
加载本地数据
'''
train_data = pd.read_csv('train.csv',sep='\t',header=None)
print(train_data.head())
# 构建数据集迭代器
def coustom_data_iter(texts,labels):for x,y in zip(texts,labels):yield x,y
# train_data[0]是第一列(通常是文本),train_data[1]是第二列(通常是标签)
train_iter = coustom_data_iter(train_data[0].values[:],train_data[1].values[:])
定义一个名为 coustom_data_iter
的函数,接收两个参数:
-
texts
:文本数据(通常是句子或单词序列) -
labels
:对应的标签(分类任务中的目标值)
for x, y in zip(texts, labels):
-
zip(texts, labels)
:将texts
和labels
按元素配对,返回一个迭代器,每次迭代返回(text, label)
的组合。 -
例如,如果
texts = ["hello", "world"]
,labels = [0, 1]
,那么zip(texts, labels)
会生成("hello", 0)
和("world", 1)
。
yield x, y
-
yield
使这个函数变成一个 生成器(generator),每次迭代返回(x, y)
对,而不是一次性返回所有数据。 -
这种方式适合大数据集,因为它不会一次性加载所有数据到内存,而是按需生成。
2.数据预处理
1)构建词典
# 中文分词方法
tokenizer = jieba.lcutdef yield_tokens(data_iter):for text,_ in data_iter:yield tokenizer(text)vocab = build_vocab_from_iterator(yield_tokens(train_iter),specials=["<unk>"])
vocab.set_default_index(vocab["<unk>"]) # 设置默认索引,如果找不到单词,则会选择默认索引label_name = list(set(train_data[1].values[:])) # 将标签去重,添加到label_name列表中
print(label_name)text_pipeline = lambda x:vocab(tokenizer(x))
label_pipeline = lambda x:label_name.index(x)
-
def yield_tokens(data_iter):
定义一个生成器函数yield_tokens
,接收一个数据迭代器data_iter
(通常是(text, label)
格式的迭代器)。 -
for text, _ in data_iter:
-
data_iter
每次返回(text, label)
,这里用_
忽略标签(因为我们只需要文本)。 -
例如,如果
data_iter
是[("hello world", 0), ("good morning", 1)]
,则text
依次是"hello world"
和"good morning"
。
-
-
yield tokenizer(text)
-
tokenizer(text)
:对文本text
进行分词(如拆分成单词列表)。 -
yield
返回分词后的结果(如["hello", "world"]
和["good", "morning"]
),逐步生成数据流。
-
-
build_vocab_from_iterator
-
是 PyTorch 的
torchtext.vocab
提供的函数,用于从迭代器构建词汇表。 -
输入:
yield_tokens(data_iter)
生成的分词结果(如["hello", "world"]
,["good", "morning"]
)。 -
输出:一个
Vocab
对象,包含所有单词到索引的映射。
-
-
specials=["<unk>"]
-
指定特殊符号
<unk>
(unknown token),用于处理词汇表中不存在的单词。 -
其他常见的特殊符号:
-
"<pad>"
:填充符号(用于统一序列长度)。 -
"<sos>"
:句子开始符。 -
"<eos>"
:句子结束符。
-
-
lambda 表达式的语法为:lambda arguments:expression 其中 arguments 是函数的参数,可以有多个参数,用逗号分隔。expression 是一个表达式,它定义了函数的返回值。 text_pipeline函数:将原始文本数据转换为整数列表,使用了之前构建的vocab词表和tokenizer分词器函数。具体来说,它接受一个字符串x作为输入,首先使用tokenizer将其分词,然后将每个词在vocab词表中的索引放入一个列表中返回。 label pipeline函数:将原始标签数据转换为整数,它接受一个字符串x作为输入,并使用 label_name.index(x)方法获取x在label name 列表中的索引作为输出。
2)生成数据批次和迭代器
# 2.生成数据批次和迭代器
def collate_batch(batch):label_list,text_list,offsets = [],[],[0]for (_text,_label) in batch:# 标签列表label_list.append(label_pipeline(_label))# 文本列表processed_text = torch.tensor(text_pipeline(_text),dtype=torch.int64)text_list.append(processed_text)# 偏移量,即语句的总词汇量offsets.append(processed_text.size(0))label_list = torch.tensor(label_list,dtype=torch.int64)text_list = torch.cat(text_list)offsets = torch.tensor(offsets[:-1]).cumsum(dim=0) # 返回维度dim中输入元素的累计和return text_list.to(device),label_list.to(device),offsets.to(device)# 数据加载器
dataloader = DataLoader(train_iter,batch_size=8,shuffle=False,collate_fn=collate_batch)
-
输入:
batch
是一个列表,其中每个元素是(_text, _label)
对(来自train_iter
)。 -
初始化:
-
label_list
:存储批次的标签。 -
text_list
:存储分词后的文本(转换为整数索引)。 -
offsets
:存储每个文本的长度(用于后续拼接),初始值为[0]
-
-
offsets
的用途:-
记录每个文本的累计长度,用于后续将多个文本拼接成一个一维张量时定位每个样本的起始位置。
-
-
label_list
:-
将标签列表转换为 PyTorch 张量(形状为
[batch_size]
)。
-
-
text_list
:-
torch.cat(text_list)
:将所有文本的索引拼接成一个一维张量。-
例如,如果有两个文本
[1, 2]
和[3, 4, 5]
,结果为[1, 2, 3, 4, 5]
。
-
-
-
offsets
:-
offsets[:-1]
:去掉初始的[0]
,保留每个文本的长度(如[2, 3]
)。 -
.cumsum(dim=0)
:计算累计和,得到每个文本在text_list
中的起始位置。-
例如,
[2, 3]
→[2, 5]
,表示:-
第一个文本在
text_list
中的位置是0:2
。 -
第二个文本的位置是
2:5
。
-
-
-
3.构建模型
首先对文本进行嵌入,然后对句子进行嵌入之后的结果进行均值整合
模型图如下:
# 1.定义模型
class TextClassificationModel(nn.Module):def __init__(self,vocab_size,embed_dim,num_class):super(TextClassificationModel,self).__init__()self.embedding = nn.EmbeddingBag(vocab_size, # 词典大小embed_dim, # 嵌入维度sparse=False)self.fc = nn.Linear(embed_dim,num_class)self.init_weights()def init_weights(self):initrange = 0.5self.embedding.weight.data.uniform_(-initrange,initrange)self.fc.weight.data.uniform_(-initrange,initrange)self.fc.bias.data.zero_()def forward(self,text,offsets):embedded = self.embedding(text,offsets)return self.fc(embedded)
self.embedding.weight.data.uniform_(-initrange,initrange)这段代码是在 PyTorch 框架下用于初始化神经网络的词嵌入层(embedding layer)权重的一种方法。这里使用了均匀分布的随机值来初始化权重,具体来说,其作用如下: self.embedding:这是神经网络中的词嵌入层(embeddinglayer)。词嵌入层的作用是将离散的单词表示(通常为整数索引)映射为固定大小的连续向量。这些向量捕捉了单词之间的语义关系,并作为网络的输入。 self.embedding.weight:这是词嵌入层的权重矩阵,它的形状为(vocab size,embedding _dim),其中 vocab size 是词汇表的大小,embedding dim 是嵌入向量的维度。
self.embedding.weight.data:这是权重矩阵的数据部分,我们可以在这里直接操作其底层的张量。 .uniform(-initrange,initrange):这是一个原地操作(in-place operation),用于将权重矩阵的值用一个均匀分布进行初始化。均匀分布的范围为[-initrange,initrange],其中 initrange 是一个正数。 通过这种方式初始化词嵌入层的权重,可以使得模型在训练开始时具有一定的随机性,有助于避免梯度消失或梯度爆炸等问题。在训练过程中,这些权重将通过优化算法不断更新,以捕捉到更好的单词表示。
# 2.定义实例
num_class = len(label_name)
vocab_size = len(vocab)
em_size = 64
model = TextClassificationModel(vocab_size,em_size,num_class).to(device)# 3.定义训练函数和评估函数
def train(dataloader):model.train()total_acc,train_loss,total_count = 0,0,0log_interval = 50start_time = time.time()for idx,(text,label,offsets) in enumerate(dataloader):predicted_label = model(text,offsets)optimzer.zero_grad() # grad属性归零loss = criterion(predicted_label,label) # 计算网络输出和真实值之间的差距loss.backward() # 反向传播nn.utils.clip_grad_norm(model.parameters(),0.1) # 梯度裁剪optimzer.step() # 每一步自动更新# 记录acc与Losstotal_acc += (predicted_label.argmax(1) == label).sum().item()train_loss += loss.item()total_count += label.size(0)if idx % log_interval == 0 and idx > 0:elapsed = time.time() - start_timeprint('| epoch {:1d} | {:4d}/{:4d} batches ''| train_acc {:4.3f} train_loss {:4.5f}'.format(epoch,idx,len(dataloader),total_acc/total_count,train_loss/total_count))total_acc,train_loss,total_count = 0,0,0start_time = time.time()def evaluate(dataloader):model.eval()total_acc, train_loss, total_count = 0, 0, 0with torch.no_grad():for idx, (text,label, offsets) in enumerate(dataloader):predicted_label = model(text, offsets)loss = criterion(predicted_label, label) # 计算网络输出和真实值之间的差距# 记录acc与Losstotal_acc += (predicted_label.argmax(1) == label).sum().item()train_loss += loss.item()total_count += label.size(0)return total_acc/total_count,train_loss/total_count
torch.nn.utils.clip_grad_norm_(model.parameters(),0.1)是一个PyTorch函数,用于在训练神经网络时限制梯度的大小。这种操作被称为梯度裁剪(gradient clipping),可以防止梯度爆炸问题,从而提高神经网络的稳定性和性能。 在这个函数中: model.parameters()表示模型的所有参数。对于一个神经网络,参数通常包括权重和偏置项。0.1是一个指定的阈值,表示梯度的最大范数(L2范数)。如果计算出的梯度范数超过这个阈值,梯度会被缩放,使其范数等于阈值。 梯度裁剪的主要日的是防止梯度爆炸。梯度爆炸通常发生在训练深度神经网络时,尤其是在处理长序列数据的循环神经网络(RNN)中。当梯度爆炸时,参数更新可能会变得非常大,导致模型无法收敛或出现数值不稳定。通过限制梯度的大小,梯度裁剪有助于解决这些问题,使模型训练变得更加稳定。
4.训练模型
1)拆分数据集运行模型
EPOCHS = 10
LR = 5
BATCH_SIZE = 64criterion = torch.nn.CrossEntropyLoss()
optimzer = torch.optim.SGD(model.parameters(),lr=LR)
scheduler = torch.optim.lr_scheduler.StepLR(optimzer,1.0,gamma=0.1)
total_accu = None# 构建数据集
train_iter = coustom_data_iter(train_data[0].values[:],train_data[1].values[:])
train_dataset = to_map_style_dataset(train_iter)
split_train_,split_valid_ = random_split(train_dataset,[int(len(train_dataset)*0.8),int(len(train_dataset)*0.2)])
train_dataloader = DataLoader(split_train_,batch_size=BATCH_SIZE,shuffle=True,collate_fn=collate_batch)
valid_dataloader = DataLoader(split_valid_,batch_size=BATCH_SIZE,shuffle=True,collate_fn=collate_batch)for epoch in range(1,EPOCHS+1):epoch_start_time = time.time()train(train_dataloader)val_acc,val_loss = evaluate(valid_dataloader)# 获取当前的学习率lr = optimzer.state_dict()['param_groups'][0]['lr']if total_accu is not None and total_accu > val_acc:scheduler.step()else:total_accu = val_accprint('-'*69)print('| epoch {:1d} | time:{:4.2f}s | ''valid_acc {:4.3f} valid_loss {:4.3f}'.format(epoch,time.time()-epoch_start_time,val_acc,val_loss))print('-'*69)
torchtext.data.functional.to_map_style_dataset 函数的作用是将一个迭代式的数据集(lterable-style dataset)转换为映射式的数据集(Map-style dataset)。这个转换使得我们可以通过索引(例如:整数)更方便地访问数据集中的元素。 在 PyTorch 中,数据集可以分为两种类型:lterable-style和 Map-style。lterable-style 数据集实现了iter_()方法,可以迭代访问数据集中的元素,但不支持通过索引访问。而 Map-style 数据集实现了__getitem()和1en()方法,可以直接通过索引访问特定元素,并能获取数据集的大小。 TorchText 是 PyTorch 的一个扩展库,专注于处理文本数据。torchtext.data.functional 中的to map style dataset 函数可以帮助我们将一个 lterable-style 数据集转换为一个易于操作的 Map-style数据集。这样,我们可以通过索引直接访问数据集中的特定样本,从而简化了训练、验证和测试过程中的数据处理。
# 2.使用测试数据集评估模型
print('Checking the results of test dataset.')
test_acc,test_loss = evaluate(valid_dataloader)
print('test accuracy {:8.3f}'.format(test_acc))# 3.测试指定数据
def predict(text,text_pipeline):with torch.no_grad():text = torch.tensor(text_pipeline(text))output = model(text,torch.tensor([0]))return output.argmax(1).item()ex_text = "随便播放一首陈奕迅的歌"
model = model.to("cpu")
print('该文本的类别是:%s'%label_name[predict(ex_text,text_pipeline)])