- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
目录
- N2 构建词典
- 1. 导入数据
- 2. 设置分词器
- 3. 去除标点和停用词
- 4. 文本迭代器
- 5. 构建词典
- 6. 文本数字化
- N3 NLP中的数据集构建
- 1. Dataset
- 2. DataLoader
- N8 使用Word2Vec进行文本分类
- 环境设置
- 数据准备
- 模型设计
- 模型训练
- 模型效果展示
- 总结与心得体会
由于K同学调整了目录,本来这应该是N6,现在是N8了,中间增加了两节内容,在这里一并打卡了。
N2 构建词典
其实在前面的章节打卡中,也涉及到了构建词典,但是没有一个完整的顺序,这节把构建词典的过程详细的梳理了一下。
1. 导入数据
使用自定义的数据,我直接从网络小说里面截取一段出来。然后按句拆开放到一个列表里。
# 数据
data = [
"我这才想起今天的米彩在上海参加了一天的商务会谈,后又不顾疲惫来酒吧救场,心中除了感谢更过意不去。",
"CC推了推我说道:“昭阳,你不送送米儿吗?”",
"我赶忙点头说道:“嗯,我送她出去。”说着便从米彩手中接过手提包帮她提着。",
"米彩说了声“谢谢”后在我之前向酒吧外走去,我跟上了她说道:“你老和我说谢谢,弄得我们之间多生分吶!”",
"“有吗?”"
"米彩心不在焉的回答,让我无从去接她的话,只是在沉默中跟着她的脚步向外面走着。",
"忽然我们的脚步止于酒吧外面的屋檐下,此刻天空竟然飘起了漫天的雪花,这个冬天终于下雪了。",
"我下意识的感叹,道:“下雪了!”",
"“嗯!”米彩应了一声,却比我更会珍惜这样的画面,从手提包里拿出卡片相机将眼前银装素裹的世界定格在了镜头里,然后离开屋檐,走进了漫天的雪花中。"
]
# 把人名弄成自定义字典
user_dictionary = [
"CC", "昭阳","米儿", "米彩"
]
2. 设置分词器
import jieba
tokenizer = jieba.lcut
jieba.load_userdict(user_dictionary)
3. 去除标点和停用词
编写一个过滤函数去除标点符号
import re
def remove_punctuation(text):
return re.sub(r'[^\w\s]', '', text)
维护一个停用词列表,编写停用词过滤函数
stopwords = ['的', '这', '是']
def remove_stopwords(words):
return [word for word in words if word not in stopwords]
4. 文本迭代器
编写一个迭代器,将文本转化成返回单词
def yield_tokens(data_iter):
for text in data_iter:
# 去除标点符号
text = remove_punctuation(text)
# 分词
words = tokenizer(text)
# 去除停用词
words = remove_stopwords(words)
yield words
5. 构建词典
from torchtext.vocab import build_vocab_from_iterator
# 遍历所有的分词结果,构建词典
vocab = build_vocab_from_iterator(yield_tokens(data), specials=['<unk>'])
# 将未知的词汇设置为<unk>
vocab.set_default_index(vocab['<unk>'])
build_vocab_from_iterator用来从一个可迭代对象中统计token的频次,并返回一个词典
它的原型如下
def build_vocab_from_iterator(iterator: Iterable,
min_freq: int = 1,
specials: Optional[List[str]] = None,
special_first: bool = True,
max_tokens: Optional[int] = None
)
参数解释如下:
- iterator: 用于创建词典的可迭代对象
- min_freq: 最小频数,文本中的词汇只有频数大于min_freq的才会保留下来,出现在词典中
- specials: 特殊标志,值是一个字符串的列表。用于在词典中添加一些特殊的token,例如我们使用<unk>来代表字典中不存在的token。
- special_first: 表示是否将special token放在词典顺序的最前面(也就是Index小),默认是True
- max_tokens: 限制词典词汇的最大数量
备注:除了special_token之外的token是按词频来降序排列的,如果两个词的频次一样,则按出现顺序。
set_default_index可以设置默认使用的token,比如添加一个<unk>设置为默认。
6. 文本数字化
# 打印词典中的内容
print('词典大小:', len(vocab))
print('词典内部映射:', vocab.get_stoi())
text = "外面是冬天"
words = remove_stopwords(jieba.lcut(text))
print()
print('jieba分词后的文本:', jieba.lcut(text))
print('去除停用词后的文本:', words)
print('数字化后的文本:', [vocab[word] for word in words])
N3 NLP中的数据集构建
torch.utils.data
是pytorch中用于数据加载和预处理的模块,其中包含Dataset
和DataLoader
两个类,通常结合使用来加载和处理数据。
1. Dataset
torch.utils.data.Dataset
是一个抽象类,用于表示数据集。
自定义Dataset需要继承这个基类,并实现两个方法__len__
和__getitem__
其中__len__
返回的是数据集的大小,__getitem__
用于根据索引返回一个数据样本。
自定义一个数据集的示例
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self, texts, labels):
self.texts = texts
self.labels = labels
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
texts = self.texts[idx]
labels = self.labels[idx]
return texts, labels
2. DataLoader
torch.utils.data.DataLoader
是pytorch中一个重要的类,用于高效的加载数据集。它可以将数据批次化、打乱数据的顺序、多线程加载数据等。
import torch
from torch.utils.data import DataLoader
# 假数据
text_data = [
torch.tensor([1, 2, 3, 4], dtype=torch.long),
torch.tensor([4, 3, 2], dtype=torch.long),
torch.tensor([1, 2], dtype=torch.long)
]
text_labels = torch.tensor([1, 0, 1], dtype=torch.float)
# 创建dataset
my_dataset = MyDataset(text_data, text_labels)
# 通过dataset创建dataloader
dataloader = DataLoader(my_dataset, batch_size=2, shuffle=True, collate_fn=lambda x: x)
# 打印一下dataloader里面的数据
for batch in dataloader:
print(batch)
重复执行几次,可以看到batch中的数据是随机的,没有固定的顺序
N8 使用Word2Vec进行文本分类
Word2Vec是一种用于生成词向量的浅层神经网络模型,由Tomas Mikolov及其团队于2013年提出。Word2Vec通过学习大量的文本数据,将每个单词表示为一个连续的向量,这些向量可以捕捉单词之间的主义和句法关系。上一节的打卡中,我们使用gensim在一篇小说内容上训练了一个Word2Vec模型。
这节我们训练一个word2vec模型,并用它来做文本分类任务。
环境设置
创建全局的torch device
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
数据准备
使用pandas读取csv
import pandas as pd
# 读取当前目录下的train.csv,里面的数据按\t分隔,并且没有表头
data = pd.read_csv('train.csv', sep='\t', header=None)
data.head()
拆分数据中的文本和标签
data_x = data[0].values[:]
data_y = data[1].values[:]
创建并训练word2vec模型
from gensim.models.word2vec import Word2Vec
# 创建一个输出向量为100维的word2vec模型,并且忽略词频小于3的单词
w2v = Word2Vec(vector_size=100, min_count=3)
# 使用数据集构建词典
w2v.build_vocab(data_x)
# 训练word2vec模型,文本数量使用上一步统计的输入文本数量(total_examples=w2v.corpus_count),训练20轮(epochs=20)
w2v.train(data_x, total_examples=w2v.corpus_count, epochs=20)
现在word2vec模型已经训练好了,我们直接把csv中的所有数据跑一下,让文本数据集变成向量数据集
import numpy as np
import os
# 把一段文本转换成向量,将其中的所有单词向量相加
def text_vector(text):
vec = np.zeros((1, 100))
for word in text:
try:
# 因为词频小于3的单词忽略了,所有会有找不到key的异常抛出这里直接try了忽略掉
vec += w2v.wv[word].reshape((1, 100))
except KeyError:
continue
return vec
# 把x转换为向量的x
vec_x = torch.concatenate([text_vector(text) for text in data_x])
# 保存word2vec模型
if not os.path.exists('model'):
os.makedirs('model', exist_ok=True)
w2v.save('model/w2v_model.pkl')
编写一个迭代函数,把x和y组合到一起
def yield_zip_iter(data_x, data_y):
for x, y in zip(data_x, data_y):
yield x, y
取出所有的分类名,用于将分类标签转成索引
label_names = list(set(data_y))
label_names
标签文本转索引的函数
label_pipeline = lambda y: label_names.index(y)
label_pipeline('Travel-Query')
编写批次数据的整理函数,用于把一个批次原本的形状如[(x1,y1), (x2,y2)]的数据转换为[x1,x2]和[y1, y2]
def collate_batch(batch):
data_list, label_list = [], []
for x, y in batch:
data_list.append(torch.tensor(x, dtype=torch.float32))
label_list.append(label_pipeline(y))
#转为tensor
data_list = torch.stack(data_list)
label_list = torch.tensor(label_list, dtype=torch.int64)
return data_list.to(device), label_list.to(device)
生成数据集
from torchtext.utils import to_map_style_dataset
from torch.utils.data import random_split
# 创建数据迭代器
data_iter = yield_zip_iter(vec_x , data_y)
# 把迭代器转成dataset
train_dataset = to_map_style_dataset(data_iter)
# 拆分dataset为训练和验证
train_size = int(len(train_dataset) * 0.8)
train_split = random_split(train_dataset, [train_size, len(train_dataset) - train_size])
模型设计
创建torch模型,只有一层简单的全连接就可以,只有一层全连接准确度不达标,增加一层bottleneck提升准确率
import torch.nn as nn
class TextClassificationModel(nn.Module):
def __init__(self, num_classes):
super().__init__()
self.fc = nn.Sequential(nn.Linear(100, 64),nn.ReLU(), nn.Linear(64, num_classes))
def forward(self, x):
return self.fc(x)
model = TextClassificationModel(len(label_names)).to(device)
模型训练
编写模型训练函数
# 编写训练函数
import time
def train(dataloader):
model.train()
train_acc, train_loss, total_count = 0, 0, 0
log_interval = 50
start_time = time.time()
for idx, (vector, label) in enumerate(dataloader):
# 前向传播
predicted_label = model(vector)
# 反向传播
optimizer.zero_grad()
loss = criterion(predicted_label, label)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 0.1) # 梯度裁剪
optimizer.step()
# 记录数据
train_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_time
print('| epoch {:1d} | {:4d}/{:4d} batches | train_acc {:4.3f} train_loss {:4.5f}'.format(epoch, idx, len(dataloader), train_acc/total_count, train_loss/total_count))
train_acc, train_acc, total_count = 0, 0, 0
start_time = time.time()
def evaluate(dataloader):
model.eval()
total_acc, total_loss, total_count = 0, 0, 0
with torch.no_grad():
for idx, (vector, label) in enumerate(dataloader):
predicted_label = model(vector)
loss = criterion(predicted_label, label)
# 记录数据
total_acc += (predicted_label.argmax(1) == label).sum().item()
total_loss += loss.item()
total_count += label.size(0)
return total_acc / total_count, total_loss / total_count
开始训练
from torch.utils.data import DataLoader
# 超参数
epochs = 10
lr = 5
batch_size = 64
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1.0, gamma=0.1)
total_acc = None
train_dataloader = DataLoader(split_train, batch_size=batch_size, shuffle=True, collate_fn=collate_batch)
valid_dataloader = DataLoader(split_valid, batch_size, shuffle=True, collate_fn=collate_batch)
acc_history, loss_history = [], []
# 训练
for epoch in range(1, epochs+1):
epoch_start_time = time.time()
train(train_dataloader)
val_acc, val_loss = evaluate(valid_dataloader)
acc_history.append(val_acc)
loss_history.append(val_loss)
lr = optimizer.state_dict()['param_groups'][0]['lr']
if total_acc is not None and total_acc > val_acc:
scheduler.step()
else:
total_acc = val_acc
print('-' * 69)
print('| epoch {:1d} | time: {:4.2f}s | valid_acc {:4.3f} valid_loss {:4.3f} | lr: {:4.6f}'.format(epoch, time.time() - epoch_start_time, val_acc, val_loss, lr))
print('-' * 69)
训练结束后打印一下模型最后的准确率
test_acc, test_loss = evaluate(valid_dataloader)
print('模型准确率为: {:5.4f}'.format(test_acc))
模型效果展示
编写一个用来预测的函数,输入文本,输出标签
def predict(text):
with torch.no_grad():
text = torch.tensor(text_pipeline(text), dtype=torch.float32)
print(text.shape)
output = model(text)
return output.argmax(1).item()
ex_text_str = '我昨天去看了沈腾演的抓娃娃'
#ex_text_str = '还有双鸭山到淮阴的汽车票吗13号的'
model = model.to('cpu')
print('该文本的类别是: %s' % label_names[predict(ex_text_str)])
可以发现结果是正确的
画一下训练过程的数据曲线
import matplotlib.pyplot as plt
ranges = list(range(1, epochs+1))
plt.title('Validation Accuracy')
plt.plot(ranges, acc_history, label='Accuracy')
plt.title('Validation Loss')
plt.plot(ranges, loss_history, label='Loss')
总结与心得体会
通过对新增的两个章节的回顾和本章的任务的训练,让我印象最深刻的就是to_map_style_dataset
这个函数的使用。通过它把一个简单的yield函数变成了数据集,具体的dataset不需要自己来实现,这样做的好处是非常快捷。但是这种方式应该仅限于数据量不大的情况下使用,如果数据量太大,它会全部加载到内存中,这时候就应该使用自定义的数据集,通过索引来进行取数据,具体的逻辑更加灵活,不用在没有训练的时候就把数据加载到内存里。
还有就是使用word2vec来实现文本分类任务的过程,开始看到这个下意识的认为需要用pytorch重写一个word2vex模型,最后发现完全可以使用不同的库组合进来达到最终的目的。