- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊 | 接辅导、项目定制
目录
- 0. 总结:
- 1.加载数据
- 2. 构建词典
- 3. 生成数据批次和迭代器
- 4.模型搭建及初始化
- 5. 定义训练与评估函数
- 6. 拆分数据集并运行模型
- 7. 结果可视化
- 8. 测试指定数据
- 9. 提问:与N4文本分类的异同
- 分析代码
- 数据处理方式
- 1. 分词处理
- 2. 构建词典
- 3. 向量表示
- 值得关注的点
- 1. Word2Vec模型训练
- 2. 文本向量化
- 3. 数据加载和批处理
- 模型结构
- 1. 模型定义
- 训练和评估
- 1. 训练过程
- 异同点总结
- 共同点
- 不同点
- 值得学习的点
0. 总结:
之前有学习过文本预处理的环节,对文本处理的主要方式有以下三种:
1:词袋模型(one-hot编码)
2:TF-IDF
3:Word2Vec(词向量(Word Embedding) 以及Word2vec(Word Embedding 的方法之一))
详细介绍及中英文分词详见pytorch文本分类(一):文本预处理
上上上上期主要介绍Embedding,及EmbeddingBag 使用示例(对词索引向量转化为词嵌入向量) ,上上上期主要介绍:应用三种模型的英文分类
上上期将主要介绍中文基本分类(熟悉流程)、拓展:textCNN分类(通用模型)、拓展:Bert分类(模型进阶)
上期主要介绍Word2Vec,和nn.Embedding()
, nn.EmbeddingBag()
相比都是嵌入技术,用于将离散的词语或符号映射到连续的向量空间。
nn.Embedding
和 nn.EmbeddingBag
是深度学习框架(如 PyTorch)中的层,直接用于神经网络模型中,而 Word2Vec
是一种独立的词嵌入算法。
使用来说,如果需要在神经网络中处理变长序列的嵌入,可以选择 nn.EmbeddingBag
;如果需要预训练词嵌入用于不同任务,可以选择 Word2Vec
。
本期主要介绍使用Word2Vec实现文本分类:
与N4文本分类的异同点总结
- 共同点
数据加载:都使用了PyTorch的DataLoader来批量加载数据。
模型训练:训练过程大同小异,都是前向传播、计算损失、反向传播和梯度更新。 - 不同点
分词处理:
BERT模型:使用专门的BERT分词器。
传统嵌入方法:通常使用jieba等工具进行分词。
Word2Vec模型:假设数据已经分词。
词向量表示:
BERT模型:使用BERT生成的上下文相关的词向量。
传统嵌入方法:使用静态预训练词向量。
Word2Vec模型:训练一个Word2Vec模型生成词向量。
模型结构:
BERT模型:使用预训练的BERT模型作为编码器。
传统嵌入方法:一般使用嵌入层+卷积/循环神经网络。
Word2Vec模型:使用Word2Vec词向量和一个简单的线性分类器。 - 值得学习的点
词向量的使用:了解如何使用Word2Vec生成词向量并将其用于下游任务。
数据预处理:不同方法的数据预处理方式,尤其是分词和词向量化的处理。
模型训练:标准的模型训练和评估流程,尤其是损失计算、反向传播和梯度更新等步骤。
超参数选择:注意学习率、批量大小和训练轮数等超参数的选择。
通过这些比较和分析,可以更好地理解不同文本分类方法的优缺点以及适用场景。
1.加载数据
import torch
import torch.nn as nn
import warnings
from torch.utils.data import DataLoader,Dataset,random_split
import pandas as pd
warnings.filterwarnings('ignore')# 忽略警告信息
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
device(type='cpu')
train_data = pd.read_csv('./data/N6-train.csv',sep='\t',header=None)
train_data
0 | 1 | |
---|---|---|
0 | 还有双鸭山到淮阴的汽车票吗13号的 | Travel-Query |
1 | 从这里怎么回家 | Travel-Query |
2 | 随便播放一首专辑阁楼里的佛里的歌 | Music-Play |
3 | 给看一下墓王之王嘛 | FilmTele-Play |
4 | 我想看挑战两把s686打突变团竞的游戏视频 | Video-Play |
... | ... | ... |
12095 | 一千六百五十三加三千一百六十五点六五等于几 | Calendar-Query |
12096 | 稍小点客厅空调风速 | HomeAppliance-Control |
12097 | 黎耀祥陈豪邓萃雯畲诗曼陈法拉敖嘉年杨怡马浚伟等到场出席 | Radio-Listen |
12098 | 百事盖世群星星光演唱会有谁 | Video-Play |
12099 | 下周一视频会议的闹钟帮我开开 | Alarm-Update |
12100 rows × 2 columns
class CustomDataset(Dataset):
def __init__(self,texts,labels):
self.texts = texts
self.labels = labels
def __len__(self):
return len(self.texts)
def __getitem__(self,idx):
return self.texts[idx],self.labels[idx]
x = train_data[0].values[:]
y = train_data[1].values[:]
2. 构建词典
from gensim.models.word2vec import Word2Vec
import numpy as np
# 训练Word2Vec 浅层神经网络模型
w2v = Word2Vec(
vector_size = 100, # 指特征向量的维度,默认为100
min_count = 3 # 可以对字典做截断,词频少于min_count次数的单词会被丢弃掉,默认值为5
)
w2v.build_vocab(x)
w2v.train(
x,
total_examples = w2v.corpus_count,
epochs = 20
)
(2733133, 3663560)
Word2Vec可以直接训练模型,一步到位。这里分了三步
●
第一步构建一个空模型
●
第二步使用 build_vocab 方法根据输入的文本数据 x 构建词典。build_vocab 方法会统计输入文本中每个词汇出现的次数,并按照词频从高到低的顺序将词汇加入词典中。
●
第三步使用 train 方法对模型进行训练,total_examples 参数指定了训练时使用的文本数量,这里使用的是 w2v.corpus_count 属性,表示输入文本的数量
如果一步到位的话代码为:
w2v = Word2Vec(x,vector_size=100,min_count=3,epochs=20)
# 将文本转化为向量
def average_vec(text):
vec = np.zeros(100).reshape((1,100))
for word in text:
try:
vec+= w2v.wv[word].reshape((1,100))
except KeyError:
continue
return vec
# 将词向量保存为Ndarray
x_vec = np.concatenate([average_vec(z) for z in x])
# 保存 Word2Vec 模型及词向量
w2v.save('data/w2v_model.pkl')
这段代码定义了一个函数 average_vec(text),它接受一个包含多个词的列表 text 作为输入,并返回这些词对应词向量的平均值。该函数
●
首先初始化一个形状为 (1, 100) 的全零 numpy 数组来表示平均向量
●
然后遍历 text 中的每个词,并尝试从 Word2Vec 模型 w2v 中使用 wv 属性获取其对应的词向量。如果在模型中找到了该词,函数将其向量加到 vec 中。如果未找到该词,函数会继续迭代下一个词
●
最后,函数返回平均向量 vec
然后使用列表推导式将 average_vec() 函数应用于列表 x 中的每个元素。得到的平均向量列表使用 np.concatenate() 连接成一个 numpy 数组 x_vec,该数组表示 x 中所有元素的平均向量。x_vec 的形状为 (n, 100),其中 n 是 x 中元素的数量。
train_iter = CustomDataset(x_vec,y)
len(x),len(x_vec)
(12100, 12100)
label_name = list(set(train_data[1].values[:]))
label_name
['Audio-Play',
'Radio-Listen',
'Travel-Query',
'TVProgram-Play',
'Video-Play',
'Other',
'Music-Play',
'Alarm-Update',
'Weather-Query',
'Calendar-Query',
'FilmTele-Play',
'HomeAppliance-Control']
3. 生成数据批次和迭代器
text_pipeline = lambda x:average_vec(x)
label_pipeline = lambda x:label_name.index(x)
lambda 表达式的语法为:lambda arguments: expression
其中 arguments 是函数的参数,可以有多个参数,用逗号分隔。expression 是一个表达式,它定义了函数的返回值。
●
text_pipeline 函数:接受一个包含多个词的列表 x 作为输入,并返回这些词对应词向量的平均值,即调用了之前定义的 average_vec 函数。这个函数用于将原始文本数据转换为词向量平均值表示的形式。
●
label_pipeline 函数:接受一个标签名 x 作为输入,并返回该标签名label_name 列表中的索引。这个函数可以用于将原始标签数据转换为数字索引表示的形式。
text_pipeline("你在干嘛")
array([[ 0.18396786, 0.24416898, 1.0977701 , -0.01573543, -2.34493342,
0.14462006, 1.09909924, 0.72848918, 0.43942198, -0.36590184,
-1.64841774, -4.0461483 , 0.52117442, -0.93646932, 0.65021983,
1.8863973 , 3.21435681, -2.37889412, 4.38834111, -1.31753699,
3.04073831, -1.56194845, -0.01702204, -0.26231994, -0.57977456,
-0.59202761, -2.09177154, -0.93535494, 2.01195888, -2.03609261,
2.05241142, 1.59645403, -0.09730234, -1.39273715, 0.35281967,
0.07837144, -0.28004935, 3.58092116, -2.69026518, 1.25223976,
0.26495122, 0.6858208 , -0.26904737, 1.87308259, 0.21092373,
0.18170499, 2.06353265, -0.55430332, -2.19337641, 1.70870071,
0.30772619, -2.93958779, -0.97791416, 0.84643018, -0.75232047,
-0.05284989, 1.25495276, -0.59528226, -0.44547228, 0.5255087 ,
0.86212044, -1.28084296, 2.38232671, -0.28152593, -0.64530418,
1.20289534, -0.42659164, 0.95189874, 1.22811846, 0.04619575,
-0.50489213, 0.27519883, 2.16535747, -0.17590564, 0.42813917,
-0.09275024, -3.63906508, 0.53455901, 2.07623281, -0.39027843,
-1.92971458, 0.42125694, -2.16986141, 2.77343564, -1.69743965,
-1.50195191, 1.76218376, -1.49796952, -0.26446094, -0.30981308,
0.9657587 , -1.63175048, 1.01593184, 0.9586434 , 0.70671193,
-2.49167663, 0.37804852, 0.94789429, -1.52659395, 0.88284026]])
label_pipeline("Travel-Query")
2
from torch.utils.data import DataLoader
def collate_batch(batch):
label_list,text_list = [],[]
for (_text,_label) in batch:
# 标签列表
label_list.append(label_pipeline(_label))
# 文本列表
processed_text = torch.tensor(text_pipeline(_text),dtype=torch.float32)
text_list.append(processed_text)
label_list = torch.tensor(label_list,dtype = torch.int64)
text_list = torch.cat(text_list)
return text_list.to(device),label_list.to(device)
4.模型搭建及初始化
from torch import nn
class TextClassificationModel(nn.Module):
def __init__(self,num_class):
super(TextClassificationModel,self).__init__()
self.fc = nn.Linear(100,num_class)
def forward(self,text):
return self.fc(text)
# 模型初始化
num_class = len(label_name)
vocab_size = 100000
em_size = 12
model = TextClassificationModel(num_class).to(device)
5. 定义训练与评估函数
def train(dataloader):
model.train() # 切换为训练模式
total_acc, total_loss, total_count = 0, 0, 0
for idx, (text,label) in enumerate(dataloader):
pred = model(text)
optimizer.zero_grad() # grad属性归零
loss = criterion(pred, label) # 计算网络输出和真实值之间的差距,label为真实值
loss.backward() # 反向传播
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1) # 梯度裁剪
optimizer.step() # 每一步自动更新
# 记录acc与loss
total_acc += (pred.argmax(1) == label).sum().item()
total_loss += loss.item()
total_count += label.size(0)
return total_acc / total_count, total_loss / total_count
def evaluate(dataloader):
model.eval() # 切换为测试模式
total_acc, total_loss, total_count = 0, 0, 0
with torch.no_grad():
for idx, (text,label) in enumerate(dataloader):
pred = model(text)
loss = criterion(pred, label) # 计算loss值
# 记录测试数据
total_acc += (pred.argmax(1) == label).sum().item()
total_loss += loss.item()
total_count += label.size(0)
return total_acc/total_count, total_loss/total_count
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1)是一个PyTorch函数,用于在训练神经网络时限制梯度的大小。这种操作被称为梯度裁剪(gradient clipping),可以防止梯度爆炸问题,从而提高神经网络的稳定性和性能。
在这个函数中:
●
model.parameters()表示模型的所有参数。对于一个神经网络,参数通常包括权重和偏置项。
●
0.1是一个指定的阈值,表示梯度的最大范数(L2范数)。如果计算出的梯度范数超过这个阈值,梯度会被缩放,使其范数等于阈值。
梯度裁剪的主要目的是防止梯度爆炸。梯度爆炸通常发生在训练深度神经网络时,尤其是在处理长序列数据的循环神经网络(RNN)中。当梯度爆炸时,参数更新可能会变得非常大,导致模型无法收敛或出现数值不稳定。通过限制梯度的大小,梯度裁剪有助于解决这些问题,使模型训练变得更加稳定。
6. 拆分数据集并运行模型
# 超参数
EPOCHS = 10 # epoch
LR = 5 # 学习率
BATCH_SIZE = 64 # batch size for training
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=LR)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1.0, gamma=0.1)
total_accu = None
# 构建数据集
train_dataset = CustomDataset(train_data[0].values[:], train_data[1].values[:])
split_train_, split_valid_ = random_split(
train_dataset,
[int(len(train_dataset)*0.8), len(train_dataset) - int(len(train_dataset)*0.8)]
)
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)
import copy
import time
epochs = 10
train_loss = []
train_acc = []
test_loss = []
test_acc = []
best_acc = 0 # 设置一个最佳准确率,作为最佳模型的判别指标
for epoch in range(epochs):
# 更新学习率(使用自定义学习率时使用)
# adjust_learning_rate(optimizer, epoch, learn_rate)
epoch_start_time = time.time()
epoch_train_acc, epoch_train_loss = train(train_dataloader)
scheduler.step() # 更新学习率(调用官方动态学习率接口时使用)
epoch_test_acc, epoch_test_loss = evaluate(valid_dataloader)
# 保存最佳模型到 best_model
if epoch_test_acc > best_acc:
best_acc = epoch_test_acc
best_model = copy.deepcopy(model)
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
# 获取当前的学习率
lr = optimizer.state_dict()['param_groups'][0]['lr']
template = ('Epoch:{:2d}, time: {:4.2f}s,Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, Lr:{:.2E}')
print(template.format(epoch+1,time.time() - epoch_start_time,epoch_train_acc*100, epoch_train_loss,
epoch_test_acc*100, epoch_test_loss, lr))
# 保存最佳模型到文件中
PATH = './best_model.pth' # 保存的参数文件名
torch.save(model.state_dict(), PATH)
print('Done')
Epoch: 1, time: 0.94s,Train_acc:79.9%, Train_loss:0.021, Test_acc:85.5%, Test_loss:0.015, Lr:5.00E-01
Epoch: 2, time: 0.91s,Train_acc:88.8%, Train_loss:0.009, Test_acc:87.8%, Test_loss:0.009, Lr:5.00E-02
Epoch: 3, time: 0.91s,Train_acc:90.1%, Train_loss:0.007, Test_acc:88.1%, Test_loss:0.008, Lr:5.00E-03
Epoch: 4, time: 0.95s,Train_acc:90.1%, Train_loss:0.007, Test_acc:88.1%, Test_loss:0.008, Lr:5.00E-04
Epoch: 5, time: 0.91s,Train_acc:90.2%, Train_loss:0.007, Test_acc:88.1%, Test_loss:0.009, Lr:5.00E-05
Epoch: 6, time: 0.88s,Train_acc:90.2%, Train_loss:0.007, Test_acc:88.1%, Test_loss:0.008, Lr:5.00E-06
Epoch: 7, time: 0.87s,Train_acc:90.2%, Train_loss:0.007, Test_acc:88.1%, Test_loss:0.008, Lr:5.00E-07
Epoch: 8, time: 0.87s,Train_acc:90.2%, Train_loss:0.007, Test_acc:88.1%, Test_loss:0.008, Lr:5.00E-08
Epoch: 9, time: 0.91s,Train_acc:90.2%, Train_loss:0.007, Test_acc:88.1%, Test_loss:0.008, Lr:5.00E-09
Epoch:10, time: 0.92s,Train_acc:90.2%, Train_loss:0.007, Test_acc:88.1%, Test_loss:0.008, Lr:5.00E-10
Done
7. 结果可视化
import matplotlib.pyplot as plt
#隐藏警告
import warnings
warnings.filterwarnings("ignore") #忽略警告信息
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 100 #分辨率
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
8. 测试指定数据
def predict(text, text_pipeline):
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_name[predict(ex_text_str, text_pipeline)])
torch.Size([1, 100])
该文本的类别是:Travel-Query
9. 提问:与N4文本分类的异同
分析代码
代码使用了Word2Vec技术将文本转化为词向量,并通过一个简单的线性分类器进行文本分类。以下是详细的分析和与之前两个模型(BERT和基于传统嵌入的方法)处理数据的方式的比较:
数据处理方式
1. 分词处理
- BERT模型:使用了BERT的分词器(
BertTokenizer
),能够处理中文字符,并将其转换为BERT所需的输入格式,包括input_ids
和attention_mask
。 - 传统嵌入方法:一般使用jieba进行分词,并通过词汇表将分词结果转换为索引。
- Word2Vec模型:在代码中直接将句子传递给Word2Vec进行训练,假设句子已经被分好词。
2. 构建词典
- BERT模型:直接使用预训练模型提供的词典(如
bert-base-chinese
)。 - 传统嵌入方法:手动构建词汇表,并将词汇映射为索引。
- Word2Vec模型:通过训练Word2Vec模型来构建词向量的词典。
3. 向量表示
- BERT模型:使用BERT生成的上下文相关的词向量。
- 传统嵌入方法:通常使用静态的预训练词向量(如Word2Vec或GloVe)。
- Word2Vec模型:代码中训练了一个浅层的Word2Vec模型,并将每个词转换为一个100维的向量,句子表示为这些词向量的平均值。
值得关注的点
1. Word2Vec模型训练
from gensim.models.word2vec import Word2Vec
import numpy as np
# 训练Word2Vec 浅层神经网络模型
w2v = Word2Vec(
vector_size=100, # 指特征向量的维度
min_count=3 # 词频少于min_count次数的单词会被丢弃
)
w2v.build_vocab(x)
w2v.train(x, total_examples=w2v.corpus_count, epochs=20)
- 训练Word2Vec模型,这里使用的是Gensim库。
vector_size
参数设置了词向量的维度,min_count
参数设置了词频少于3次的单词会被丢弃。
2. 文本向量化
# 将文本转化为向量
def average_vec(text):
vec = np.zeros(100).reshape((1, 100))
for word in text:
try:
vec += w2v.wv[word].reshape((1, 100))
except KeyError:
continue
return vec
# 将词向量保存为Ndarray
x_vec = np.concatenate([average_vec(z) for z in x])
- 使用Word2Vec模型将每个词转换为向量,并计算句子的平均向量。这里的句子表示为其包含词的词向量的平均值。
3. 数据加载和批处理
from torch.utils.data import DataLoader
def collate_batch(batch):
label_list, text_list = [], []
for (_text, _label) in batch:
# 标签列表
label_list.append(label_pipeline(_label))
# 文本列表
processed_text = torch.tensor(text_pipeline(_text), dtype=torch.float32)
text_list.append(processed_text)
label_list = torch.tensor(label_list, dtype=torch.int64)
text_list = torch.cat(text_list)
return text_list.to(device), label_list.to(device)
- 这里的
collate_batch
函数用于将数据批处理成训练时所需的格式。text_pipeline
和label_pipeline
分别用于处理文本和标签。
模型结构
1. 模型定义
class TextClassificationModel(nn.Module):
def __init__(self, num_class):
super(TextClassificationModel, self).__init__()
self.fc = nn.Linear(100, num_class)
def forward(self, text):
return self.fc(text)
- 这是一个简单的线性分类器,输入是100维的词向量,输出是分类的结果。
训练和评估
1. 训练过程
def train(dataloader):
model.train() # 切换为训练模式
total_acc, total_loss, total_count = 0, 0, 0
for idx, (text, label) in enumerate(dataloader):
pred = model(text)
optimizer.zero_grad() # grad属性归零
loss = criterion(pred, label) # 计算网络输出和真实值之间的差距,label为真实值
loss.backward() # 反向传播
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1) # 梯度裁剪
optimizer.step() # 每一步自动更新
# 记录acc与loss
total_acc += (pred.argmax(1) == label).sum().item()
total_loss += loss.item()
total_count += label.size(0)
return total_acc / total_count, total_loss / total_count
- 标准的训练过程,包括前向传播、计算损失、反向传播和梯度更新。
异同点总结
共同点
- 数据加载:都使用了PyTorch的DataLoader来批量加载数据。
- 模型训练:训练过程大同小异,都是前向传播、计算损失、反向传播和梯度更新。
不同点
-
分词处理:
- BERT模型:使用专门的BERT分词器。
- 传统嵌入方法:通常使用jieba等工具进行分词。
- Word2Vec模型:假设数据已经分词。
-
词向量表示:
- BERT模型:使用BERT生成的上下文相关的词向量。
- 传统嵌入方法:使用静态预训练词向量。
- Word2Vec模型:训练一个Word2Vec模型生成词向量。
-
模型结构:
- BERT模型:使用预训练的BERT模型作为编码器。
- 传统嵌入方法:一般使用嵌入层+卷积/循环神经网络。
- Word2Vec模型:使用Word2Vec词向量和一个简单的线性分类器。
值得学习的点
- 词向量的使用:了解如何使用Word2Vec生成词向量并将其用于下游任务。
- 数据预处理:不同方法的数据预处理方式,尤其是分词和词向量化的处理。
- 模型训练:标准的模型训练和评估流程,尤其是损失计算、反向传播和梯度更新等步骤。
- 超参数选择:注意学习率、批量大小和训练轮数等超参数的选择。
通过这些比较和分析,可以更好地理解不同文本分类方法的优缺点以及适用场景。