自然语言处理入门
新闻主题分类任务
- 以一段新闻中的文本描述内容作为输入,使用模型判断最有可能属于哪一种类型新闻。假定每种类型是互斥的,即文本描述有且只有一种类别。
# 通过torchtext获取数据
import torch
import torchtext
# 导入torchtext.datasets中的文本分类任务
import torchtext.datasets
import os
path = './data'
if not os.path.isdir(path):
os.mkdir(path)
# 将文本分类数据集'AG_NEWS'保存在指定目录
train_dataset, test_dataset = torchtext.datasets.DATASETS['AG_NEWS'](root=path)
-
train.csv表示训练数据,共12万条数据,共由三列组成:标签、新闻标题、新闻简述,标签用1、2、3、4表示,依次对应classes中的
['World', 'Sports', 'Business', 'Sci/Tech']
。 -
整个案例的实现可以分为五个步骤
- 构建带有Embedding层的文本分类模型:
- 对数据进行batch处理
- 构建训练与验证函数
- 进行模型训练和验证
- 查看Embedding层嵌入的词向量
构建带有Embedding层的文本分类模型
# 导入必备的torch模型构建工具
import torch.nn as nn # nn指的是neutral network
import torch.nn.functional as F
# 指定批次训练BATCH_SIZE大小
BATCH_SIZE = 16
# 进行可用设备检测
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
class TextSentiment(nn.Module):
'''文本分类模型'''
def __init__(self, vocab_size, embed_dim, num_class):
'''
Description : 类的初始化函数
Parameters
----------
vocab_size :
整个语料包含的不同词汇整数.
embed_dim :
指定词嵌入的维度.
num_class :
文本分类的类别总数.
Returns
-------
None.
'''
super().__init__()
# 实例化embedding层、sparse=True代表每次对该层求解梯度时,只更新部分权重
self.embedding = nn.Embedding(vocab_size, embed_dim, sparse=True)
# 实例化线性层,参数分别是embed_dim和num_class
self.fc = nn.Linear(embed_dim, num_class)
# 为各层初始化权重
self.init_weights()
def init_weights(self):
'''
Description : 初始化权重函数
Returns
-------
None.
'''
# 指定初始权重的取值范围
initrange = 0.5
# 各层的权重参数都是初始化为均匀分布
self.embedding.weight.data.uniform_(-initrange, initrange)
self.fc.weight.data.uniform_(-initrange, initrange)
# 偏置初始化为0
self.fc.bias.data.zero_()
def forward(self, text):
'''
Description : nn主要逻辑函数
Parameters
----------
text :
文本数值映射后的结果.
Returns
-------
与类别数尺寸相同的张量,用以判断文本类别.
'''
# 执行词嵌入
embedded = self.embedding(text)
# 将(m,32)转化成(BATCH_SIZE, 32)
c = embedded.size(0) // BATCH_SIZE
# 使新的embedded中的向量个数可以整除BATCH_SIZE
embedded = embedded[:BATCH_SIZE*c]
# 利用平均池化的方法求embedded中指定行数的列的平均数
embedded = embedded.transpose(1, 0).unsqueeze(0)
# 调用平均池化的方法,并且核的大小为c
embedded = F.avg_pool1d(embedded, kernel_size=c)
# 减去新增的维度,转置回去输送给fc层
return self.fc(embedded[0].transpose(1,0))
# 获取整个语料中词汇总数
VOCAB_SIZE = len(train_dataset.get_vocab())
# 指定词嵌入维度
EMBED_DIM = 32
# 获取整个文本分类总数
NUM_CLASS = len(train_dataset.get_labels())
# 实例化模型对象
model = TextSentiment(VOCAB_SIZE, EMBED_DIM, NUM_CLASS).to(device)
对数据进行batch处理
def generate_batch(batch):
'''
Description : 生成batch数据函数
Parameters
----------
batch : TYPE
由样本张量和对应标签的元组组成的batch_size大小的列表.
Returns
-------
样本张量和标签各自的列表形式(张量).
'''
# 从batch中获取标签张量
label = torch.tensor([entry[1] for entry in batch])
# 从batch中获得样本张量
text = [entry[0] for entry in batch]
text = torch.cat(text)
# 返回结果
return text, label
构建模型训练和验证函数
# 导入torch中数据加载器方法
from torch.utils.data import DataLoader
def train(train_data):
'''
Description : 模型训练函数
Parameters
----------
train_data : TYPE
要训练的数据集.
Returns
-------
本轮训练的平均损失率和平均准确率.
'''
# 初始化训练损失率和准确率为0
train_loss = 0
train_acc = 0
# 使用数据加载器生成BATCH_SIZE大小的数据进行批次训练
# data是N个generate_batch函数处理后的BATCH_SIZE大小的数据生成器
data = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True,
collate_fn=generate_batch)
# 对data进行循环遍历,使用每个batch的数据进行参数更新
for i, (text, cls) in enumerate(data):
# 设置优化器初始梯度0
optimizer.zero_grad()
# 模型输入一个批次数据,获得输出
output = model(text)
# 根据真实标签与模型输出计算损失
loss = criterion(output, cls)
# 将该批次损失加到总损失中
train_loss += loss.item()
# 误差反向传播
loss.backward()
# 参数更新
optimizer.step()
# 将该批次准确率加入总准确率中
train_acc += (output.argmax(1) == cls).sum().item()
# 调整优化器学习率
scheduler.step()
# 返回本轮训练平均损失和平均准确率
return train_loss / len(train_data) , train_acc / len(train_data)
def valid(valid_data):
'''
Description : 模型验证函数
Parameters
----------
valid_data :
训练集.
Returns
-------
返回本轮训练平均损失和平均准确率.
'''
# 初始化训练损失率和准确率为0
loss = 0
acc = 0
# 和训练相同,使用DataLoader获得训练数据生成器
data = DataLoader(valid_data, batch_size=BATCH_SIZE, collate_fn=generate_batch)
# 按批次取出验证
for text, cls in data:
# 不再求解梯度
with torch.no_grad():
# 使用模型获得输出
output = model(text)
# 计算损失
loss = criterion(output, cls)
# 将损失和准确率加总
loss += loss.item()
acc += (output.argmax(1) == cls).sum().item()
# 返回本轮验证的平均损失和平均准确率
return loss / len(valid_data), acc / len(valid_data)
进行模型训练和验证
# 导入时间工具包
import time
# 导入数据随机划分工具
from torch.utils.data.dataset import random_split
# 指定训练轮数
N_EPOCHS = 10
# 定义初始的验证损失
min_valid_loss = float('inf')
# 选择损失函数,这里选择预定义的交叉熵损失函数
criterion = torch.nn.CrossEntropyLoss().to(device)
# 选择随机梯度下降优化器
optimizer = torch.optim.SGD(model.parameters(), lr=4.0)
# 选择优化器步长调节方法StepLR,用来衰减学习率
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.9)
# 从train_dataset取出0.95作为训练集,先取其长度
train_len = int(len(train_dataset) * 0.95)
# 然后使用random_split进行乱序划分,得到对应的训练集和验证集
sub_train_, sub_valid_ = \
random_split(train_dataset, [train_len, len(train_dataset) - train_len])
# 开始每一轮训练
for epoch in range(N_EPOCHS):
# 记录概论训练的开始时间
start_time = time.time()
# 调用train和valid函数得到训练和验证的平均损失,平均准确率
train_loss, train_acc = train(sub_train_)
valid_loss, valid_acc = valid(sub_valid_)
# 计算训练和验证总耗时
secs = int(time.time()) - start_time
# 换算成分秒
mins = secs/60
secs = secs%60
# 打印训练和验证耗时,平均损失、平均损失率
print('Epoch: %d' % (epoch + 1), ' | time in %d minutes, %d seconds' %(mins, secs))
print(f'\tLoss: {
train_loss:.4f}(train)\t|\5Acc: {
train_acc * 100:.1f}%(train)')
print(f'\tLoss: {
valid_loss:.4f}(valid)\t|\5Acc: {
valid_acc * 100:.1f}%(valid)')
整体代码和运行结果
# 导入相关的torch工具包
import torch
# 导入torchtext.datasets中的文本分类任务
from torchtext.datasets import text_classification
import os
# 定义数据下载路径, 当前路径的data文件夹
load_data_path = "./data"
# 如果不存在该路径, 则创建这个路径
if not os.path.isdir(load_data_path):
os.mkdir(load_data_path)
# 选取torchtext中的文本分类数据集'AG_NEWS'即新闻主题分类数据, 保存在指定目录下
# 并将数值映射后的训练和验证数据加载到内存中
train_dataset, test_dataset = text_classification.DATASETS['AG_NEWS'](root=load_data_path, vocab=None)
# 导入必备的torch模型构建工具
import torch.nn as nn # nn指的是neutral network
import torch.nn.functional as F
# 指定批次训练BATCH_SIZE大小
BATCH_SIZE = 16
# 进行可用设备检测
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
class TextSentiment(nn.Module):
'''文本分类模型'''
def __init__(self, vocab_size, embed_dim, num_class):
'''
Description : 类的初始化函数
Parameters
----------
vocab_size :
整个语料包含的不同词汇整数.
embed_dim :
指定词嵌入的维度.
num_class :
文本分类的类别总数.
Returns
-------
None.
'''
super().__init__()
# 实例化embedding层、sparse=True代表每次对该层求解梯度时,只更新部分权重
self.embedding = nn.Embedding(vocab_size, embed_dim, sparse=True)
# 实例化线性层,参数分别是embed_dim和num_class
self.fc = nn.Linear(embed_dim, num_class)
# 为各层初始化权重
self.init_weights()
def init_weights(self):
'''
Description : 初始化权重函数
Returns
-------
None.
'''
# 指定初始权重的取值范围
initrange = 0.5
# 各层的权重参数都是初始化为均匀分布
self.embedding.weight.data.uniform_(-initrange, initrange)
self.fc.weight.data.uniform_(-initrange, initrange)
# 偏置初始化为0
self.fc.bias.data.zero_()
def forward(self, text):
'''
Description : nn主要逻辑函数
Parameters
----------
text :
文本数值映射后的结果.
Returns
-------
与类别数尺寸相同的张量,用以判断文本类别.
'''
# 执行词嵌入
embedded = self.embedding(text)
# 将(m,32)转化成(BATCH_SIZE, 32)
c = embedded.size(0) // BATCH_SIZE
# 使新的embedded中的向量个数可以整除BATCH_SIZE
embedded = embedded[:BATCH_SIZE*c]
# 利用平均池化的方法求embedded中指定行数的列的平均数
embedded = embedded.transpose(1, 0).unsqueeze(0)
# 调用平均池化的方法,并且核的大小为c
embedded = F.avg_pool1d(embedded, kernel_size=c)
# 减去新增的维度,转置回去输送给fc层
return self.fc(embedded[0].transpose(1,0))
# 获取整个语料中词汇总数
VOCAB_SIZE = len(train_dataset.get_vocab())
# 指定词嵌入维度
EMBED_DIM = 32
# 获取整个文本分类总数
NUM_CLASS = len(train_dataset.get_labels())
# 实例化模型对象
model = TextSentiment(VOCAB_SIZE, EMBED_DIM, NUM_CLASS).to(device)
# 对数据进行batch处理
def generate_batch(batch):
'''
Description : 生成batch数据函数
Parameters
----------
batch : TYPE
由样本张量和对应标签的元组组成的batch_size大小的列表.
Returns
-------
样本张量和标签各自的列表形式(张量).
'''
# 从batch中获取标签张量
label = torch.tensor([entry[0] for entry in batch])
# 从batch中获得样本张量
text = [entry[1] for entry in batch]
text = torch.cat(text)
# 返回结果
return text, label
# 导入torch中数据加载器方法
from torch.utils.data import DataLoader
def train(train_data):
'''
Description : 模型训练函数
Parameters
----------
train_data : TYPE
要训练的数据集.
Returns
-------
本轮训练的平均损失率和平均准确率.
'''
# 初始化训练损失率和准确率为0
train_loss = 0
train_acc = 0
# 使用数据加载器生成BATCH_SIZE大小的数据进行批次训练
# data是N个generate_batch函数处理后的BATCH_SIZE大小的数据生成器
data = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True,
collate_fn=generate_batch)
# 对data进行循环遍历,使用每个batch的数据进行参数更新
for i, (text, cls) in enumerate(data):
# 设置优化器初始梯度0
optimizer.zero_grad()
# 模型输入一个批次数据,获得输出
output = model(text)
# 根据真实标签与模型输出计算损失
loss = criterion(output, cls)
# 将该批次损失加到总损失中
train_loss += loss.item()
# 误差反向传播
loss.backward()
# 参数更新
optimizer.step()
# 将该批次准确率加入总准确率中
train_acc += (output.argmax(1) == cls).sum().item()
# 调整优化器学习率
scheduler.step()
# 返回本轮训练平均损失和平均准确率
return train_loss / len(train_data) , train_acc / len(train_data)
def valid(valid_data):
'''
Description : 模型验证函数
Parameters
----------
valid_data :
训练集.
Returns
-------
返回本轮训练平均损失和平均准确率.
'''
# 初始化训练损失率和准确率为0
loss = 0
acc = 0
# 和训练相同,使用DataLoader获得训练数据生成器
data = DataLoader(valid_data, batch_size=BATCH_SIZE, collate_fn=generate_batch)
# 按批次取出验证
for text, cls in data:
# 不再求解梯度
with torch.no_grad():
# 使用模型获得输出
output = model(text)
# 计算损失
loss = criterion(output, cls)
# 将损失和准确率加总
loss += loss.item()
acc += (output.argmax(1) == cls).sum().item()
# 返回本轮验证的平均损失和平均准确率
return loss / len(valid_data), acc / len(valid_data)
# 导入时间工具包
import time
# 导入数据随机划分工具
from torch.utils.data.dataset import random_split
# 指定训练轮数
N_EPOCHS = 10
# 定义初始的验证损失
min_valid_loss = float('inf')
# 选择损失函数,这里选择预定义的交叉熵损失函数
criterion = torch.nn.CrossEntropyLoss().to(device)
# 选择随机梯度下降优化器
optimizer = torch.optim.SGD(model.parameters(), lr=4.0)
# 选择优化器步长调节方法StepLR,用来衰减学习率
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.9)
# 从train_dataset取出0.95作为训练集,先取其长度
train_len = int(len(train_dataset) * 0.95)
# 然后使用random_split进行乱序划分,得到对应的训练集和验证集
sub_train_, sub_valid_ = \
random_split(train_dataset, [train_len, len(train_dataset) - train_len])
# 开始每一轮训练
for epoch in range(N_EPOCHS):
# 记录概论训练的开始时间
start_time = time.time()
# 调用train和valid函数得到训练和验证的平均损失,平均准确率
train_loss, train_acc = train(sub_train_)
valid_loss, valid_acc = valid(sub_valid_)
# 计算训练和验证总耗时
secs = int(time.time()) - start_time
# 换算成分秒
mins = secs/60
secs = secs%60
# 打印训练和验证耗时,平均损失、平均损失率
print('Epoch: %d' % (epoch + 1), ' | time in %d minutes, %d seconds' %(mins, secs))
print(f'\tLoss: {
train_loss:.4f}(train)\t|\5Acc: {
train_acc * 100:.1f}%(train)')
print(f'\tLoss: {
valid_loss:.4f}(valid)\t|\5Acc: {
valid_acc * 100:.1f}%(valid)')
- 运行结果如下:
120000lines [00:13, 9115.61lines/s]
120000lines [00:20, 5871.92lines/s]
7600lines [00:01, 5522.88lines/s]
Epoch: 1 | time in 0 minutes, 43 seconds
Loss: 0.0591(train) |Acc: 64.1%(train)
Loss: 0.0004(valid) |Acc: 69.6%(valid)
Epoch: 2 | time in 0 minutes, 43 seconds
Loss: 0.0510(train) |Acc: 71.2%(train)
Loss: 0.0003(valid) |Acc: 70.1%(valid)
Epoch: 3 | time in 0 minutes, 51 seconds
Loss: 0.0479(train) |Acc: 73.3%(train)
Loss: 0.0004(valid) |Acc: 71.0%(valid)
Epoch: 4 | time in 0 minutes, 45 seconds
Loss: 0.0466(train) |Acc: 74.0%(train)
Loss: 0.0004(valid) |Acc: 69.9%(valid)
Epoch: 5 | time in 0 minutes, 44 seconds
Loss: 0.0453(train) |Acc: 74.9%(train)
Loss: 0.0004(valid) |Acc: 70.7%(valid)
Epoch: 6 | time in 0 minutes, 41 seconds
Loss: 0.0449(train) |Acc: 75.2%(train)
Loss: 0.0004(valid) |Acc: 71.2%(valid)
Epoch: 7 | time in 0 minutes, 43 seconds
Loss: 0.0444(train) |Acc: 75.3%(train)
Loss: 0.0004(valid) |Acc: 70.6%(valid)
Epoch: 8 | time in 0 minutes, 49 seconds
Loss: 0.0437(train) |Acc: 76.0%(train)
Loss: 0.0004(valid) |Acc: 71.3%(valid)
Epoch: 9 | time in 0 minutes, 44 seconds
Loss: 0.0430(train) |Acc: 76.4%(train)
Loss: 0.0004(valid) |Acc: 71.2%(valid)
Epoch: 10 | time in 0 minutes, 42 seconds
Loss: 0.0425(train) |Acc: 76.7%(train)
Loss: 0.0004(valid) |Acc: 71.6%(valid)
查看embedding层嵌入的词向量
# 打印从模型的状态字典中获得的Embedding矩阵
print(model.state_dict()['embedding.weight'])
tensor([[ 0.4049, -0.1971, 0.3739, ..., 0.1506, 0.3608, 0.3898],
[ 0.2040, -0.4809, 0.3838, ..., -0.0592, -0.4796, 0.1442],
[ 0.0912, 0.2452, 0.0459, ..., 0.1351, 0.0379, -0.0419],
...,
[-0.3961, -0.3199, -0.2815, ..., 0.4672, -0.3334, 0.4858],
[ 0.3872, -0.2327, -0.0710, ..., 0.3487, 0.4416, -0.2740],
[ 0.1301, 0.2633, -0.3815, ..., -0.1415, 0.1318, -0.4704]])
发现问题及解决方案
-
torchtext版本太新,很多内容发生了改动,使用
pip install torchtext==0.4
下载0.4.x版本即可。 -
下载数据集时,由于是外网,会一直出现服务器拒绝连接,打开AG_NEWS类,看到其返回值调用了
_setup_datasets(*("AG_NEWS",) + args), **kwargs)
方法,找到该方法,发现在第一行dataset_tar = download_from_url(URLS[dataset_name], root=root
正是由于download_from_url导致我们的请求一直被deny。这个方法的返回值是path,所以我们直接写入dataset_tar = './data/ag_news_csv.tgz'
将本地文件地址返回即可。 -
再次运行时,出现了
OverflowError: Python int too large to convert to C long
的问题,查到该问题出现在unicode_csv_reader()
方法内的csv.field_size_limit(sys.maxsize)
,将这一段代码注释掉即可。