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中用于数据加载和预处理的模块,其中包含DatasetDataLoader两个类,通常结合使用来加载和处理数据。

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中的数据是随机的,没有固定的顺序
数据集展示1
数据集展示2

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)

word2vex训练输出
现在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模型,最后发现完全可以使用不同的库组合进来达到最终的目的。

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值