少女祈祷中...

友情提示:由于HuggingFace社区触犯了天朝的某些法律,有关HuggingFace系列的内容中,提到“冲浪板”就指科学上网,需要借助国外旅游工具。

任务简介

情感分类问题属于是自然语言处理中的”Hello World”,目的就是对一段话的情感进行分类。最简单的就是二分类,包含了积极和消极两种情感。

数据集

在本次任务中使用的数据集为ChnSentiCorp数据集。数据集包含了9600条训练集、1200条测试集、1200条验证集。每条数据包含了两个属性,分别是text和label。其中text是一段中文文本,代表一段评价,label是0或1的数字标签,分别代表了消极和积极。

那么,该怎样获取这个数据集呢?
正常来讲,我们只需要这样一段代码,就可以直接从HuggingFace上获取数据集。

1
2
from datasets import load_dataset
dataset = load_dataset("seamew/ChnSentiCorp")

但是!这个数据集被放在了谷歌云上,我们没法直接获取,即便有冲浪板也不好用。不过不用担心,我们可以站在巨人的肩膀上看世界。

开始实战

加载编码工具

注意,第一次使用需要冲浪板。

1
2
3
4
5
6
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
pretrained_model_name_or_path='bert-base-chinese',
cache_dir=None,
force_download=False,
)

加载和观察数据集

1
2
3
4
from datasets import load_from_disk
dataset = load_from_disk('./data/ChnSentiCorp/ChnSentiCorp')
dataset
dataset['train'][0]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 9600
})
test: Dataset({
features: ['text', 'label'],
num_rows: 1200
})
validation: Dataset({
features: ['text', 'label'],
num_rows: 1200
})
})
{'text': '选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。 服务吗,一般', 'label': 1}

我们在后面要使用Pytorch的工具建立模型和训练,所以这里我们建立一个Pytorch可以直接使用的数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch

class Dataset(torch.utils.data.Dataset):
def __init__(self,split):
self.dataset = load_from_disk('./data/ChnSentiCorp/ChnSentiCorp')[split]
#注意数据路径,保证最后的路径文件夹下有一个ChnSentiCorp.json文件就行。
def __len__(self):
return len(self.dataset)

def __getitem__(self,i):
text = self.dataset[i]['text']
label = self.dataset[i]['label']
return text,label
#获取训练集
train_set = Dataset('train')
#获取一下GPU。
def try_gpu(i=0):
if torch.cuda.device_count() >= i + 1:
return torch.device(f'cuda:{i}')
return torch.device('cpu')
device = try_gpu()

数据整理函数

我们需要将文本数据编码后以张量的格式存储在GPU上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def collate_fn(data):
sents = [i[0] for i in data]
labels =[i[1] for i in data]

out = tokenizer.batch_encode_plus(
batch_text_or_text_pairs=sents,
add_special_tokens=True, #如果return_attention_mask为True,那么这个也要写True。
truncation=True, #当句子长度大于max_length时截断。
padding='max_length', #当句子长度小于max_length时补PAD。
max_length=500,
return_tensors='pt', #直接返回pytorch的张量。
return_token_type_ids=True,
return_attention_mask=True, #返回attention_mask,即标注句子中的补足符号[PAD],1代表[PAD],其余均为0。
return_special_tokens_mask=False,
return_length=True
)

input_ids = out['input_ids']
attention_mask = out['attention_mask']
token_type_ids = out['token_type_ids']
labels = torch.LongTensor(labels)

input_ids = input_ids.to(device)
attention_mask = attention_mask.to(device)
token_type_ids = token_type_ids.to(device)
labels = labels.to(device)
return input_ids, attention_mask, token_type_ids, labels

接下来我们不着急进行下一步,可以先进行试算。随便弄一些文本和标签,将它们放入函数,看一看结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
demo = [
('你站在桥上看风景', 1),
('看风景的人在楼上看你', 0),
('明月装饰了你的窗子', 1),
('你装饰了别人的梦', 0),
]
input_ids, attention_mask, token_type_ids, labels = collate_fn(demo)

input_ids
attention_mask
token_type_ids
labels
input_ids.shape,attention_mask.shape,token_type_ids.shape,labels.shape
1
2
3
4
5
6
7
8
9
10
11
12
13
14
tensor([[ 101,  872, 4991,  ...,    0,    0,    0],
[ 101, 4692, 7599, ..., 0, 0, 0],
[ 101, 3209, 3299, ..., 0, 0, 0],
[ 101, 872, 6163, ..., 0, 0, 0]], device='cuda:0')
tensor([[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0]], device='cuda:0')
tensor([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], device='cuda:0')
tensor([1, 0, 1, 0], device='cuda:0')
(torch.Size([4, 500]), torch.Size([4, 500]), torch.Size([4, 500]), torch.Size([4]))

注意,我们并不在意试算后的具体数值结果是多少。相比而言,我们更看重输出的数据格式,这一点非常重要!

数据加载器

现在的输出格式看上去没什么大问题,下一步就是构建加载器。

1
2
3
4
5
6
7
train_load = torch.utils.data.DataLoader(
dataset=train_set,
batch_size=16,
collate_fn=collate_fn, #指定数据整理函数。
shuffle=True,
drop_last=True) #当剩余数据不足一个批量大小时,舍弃。
len(train_load)
1
600

预训练模型

此处加载的模型为bert-base-chinese模型,和编码工具的名字一致,因为模型和其编码工具往往配套使用。

1
2
3
from transformers import BertModel
pretrained = BertModel.from_pretrained('bert-base-chinese')
sum(i.numel() for i in pretrained.parameters()) / 10000
1
10226.7648

顺便可以查看一下模型的参数数量,一共是约为一亿个参数。
由于这个模型较大,并且已经是训练好的模型,并且它只负责特征提取工作,所以在本次任务中可以不对其再次进行训练。为了节省计算,可以将其梯度冻结。

1
2
3
4
5
6
7
for param in pretrained.parameters():
param.requires_grad_(False)
#将模型转移到GPU上。
pretrained.to(device)
#进行一次试算。
out = pretrained(input_ids=input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids)
out.last_hidden_state.shape
1
torch.Size([4, 500, 768])

这个数据的格式表示,模型将之前的四段文本的编码结果也就是[4,500]张量,变成了[4,500,768]的张量,也就是说,最初的每段文本,都变成了[500,768]的张量,这就是属于这个文本的特征。

下游任务模型

在获取到特征以后,下游的模型只需要将特征转为0和1标签的概率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch.nn as nn

class Model(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(768, 2)

def forward(self, input_ids, attention_mask, token_type_ids):
#使用预训练模型抽取数据特征
with torch.no_grad(): #我们不对Bert模型进行训练,所以这里就省去梯度的操作。
out = pretrained(input_ids=input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids)
#对抽取的特征只取第1个字的结果做分类即可
out = self.fc1(out.last_hidden_state[:, 0])
return out

在这段代码中,定义了下游任务模型,该模型只包括一个全连接的线性神经网络,权重矩阵为768×2,所以它能够把一个768维度的向量转换到二维空间中。
下游任务模型的计算过程为,获取了一批数据之后,使用backbone将这批数据抽取成特征矩阵,抽取的特征矩阵的形状应该是16×500×768,这在之前预训练模型的试算中已经看到。这3个维度分别代表了16句话、500个词、768维度的特征向量。之后下游任务模型丢弃了499个词的特征,只取得第1个词(索引为0)的特征向量,对应编码结果中的[CLS],把特征向量矩阵变成了16×768。相当于把每句话变成了一个768维度的向量。
注意:之所以只取了第0个词的特征做后续的判断计算,这和预训练模型BERT的训练方法有关系,这部分涉及到Bert的原理。

然后可以对下游模型进行试算。

1
2
3
model = Model()
model.to(device)
model(input_ids=input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids).shape
1
torch.Size([4, 2])

很好,输出的形状符合预期。

训练模型

所有东西都准备好了,可以开始训练了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#这里选用的是AdamW优化器,它是Adam的优化版,在自然语言处理处理领域一般比Adam更好用。
from transformers import AdamW
from transformers.optimization import get_scheduler

def Train():
optimizer = AdamW(model.parameters(),lr=1e-2)
loss_fn = nn.CrossEntropyLoss() #选用交叉熵损失函数,分类任务一般都这么选。

#定义学习率调节器。
scheduler = get_scheduler(name='linear',num_warmup_steps=3,num_training_steps=len(train_load),optimizer=optimizer)
model.train() #将模型切换到训练模式。

#遍历数据
for i, (input_ids, attention_mask, token_type_ids,labels) in enumerate(train_load):

#计算模型预测结果。
out = model(input_ids=input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids)
loss = loss_fn(out,labels) #计算损失。

#经典老三行,多加了学习率的计算。
optimizer.zero_grad()
loss.backward()
optimizer.step()

scheduler.step()

#输出训练日志。
if i%10 == 0:
out = out.argmax(dim=1)
accuracy = (out == labels).sum().item() / len(labels)
lr = optimizer.state_dict()['param_groups'][0]['lr']
print("steps:",i,"loss:",loss.item(),"lr:",lr,"accuracy:",accuracy)

Train()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
steps: 0 loss: 0.9177090525627136 lr: 0.003333333333333333 accuracy: 0.3125
steps: 10 loss: 0.2582670748233795 lr: 0.009865996649916248 accuracy: 0.875
steps: 20 loss: 0.7411776185035706 lr: 0.009698492462311558 accuracy: 0.6875
steps: 30 loss: 0.4699391722679138 lr: 0.009530988274706867 accuracy: 0.75
steps: 40 loss: 0.5517882108688354 lr: 0.009363484087102178 accuracy: 0.8125
steps: 50 loss: 0.13808736205101013 lr: 0.009195979899497487 accuracy: 0.9375
steps: 60 loss: 0.06214526295661926 lr: 0.009028475711892798 accuracy: 1.0
steps: 70 loss: 0.2960442900657654 lr: 0.008860971524288106 accuracy: 0.9375
steps: 80 loss: 0.19490642845630646 lr: 0.008693467336683417 accuracy: 0.875
steps: 90 loss: 0.24142661690711975 lr: 0.008525963149078728 accuracy: 0.9375
steps: 100 loss: 0.17917616665363312 lr: 0.008358458961474037 accuracy: 0.9375
steps: 110 loss: 0.10000098496675491 lr: 0.008190954773869347 accuracy: 0.9375
steps: 120 loss: 0.28352534770965576 lr: 0.008023450586264656 accuracy: 0.9375
steps: 130 loss: 0.2868281602859497 lr: 0.007855946398659967 accuracy: 0.875
steps: 140 loss: 0.40539053082466125 lr: 0.007688442211055277 accuracy: 0.8125
steps: 150 loss: 0.3733450472354889 lr: 0.0075209380234505865 accuracy: 0.875
steps: 160 loss: 0.30549660325050354 lr: 0.007353433835845896 accuracy: 0.875
steps: 170 loss: 0.3883017301559448 lr: 0.007185929648241206 accuracy: 0.8125
steps: 180 loss: 0.2768547832965851 lr: 0.007018425460636516 accuracy: 0.8125
steps: 190 loss: 0.29774126410484314 lr: 0.006850921273031826 accuracy: 0.875
steps: 200 loss: 0.30697885155677795 lr: 0.0066834170854271355 accuracy: 0.875
steps: 210 loss: 0.24537579715251923 lr: 0.006515912897822446 accuracy: 0.8125
steps: 220 loss: 0.4430205523967743 lr: 0.006348408710217756 accuracy: 0.9375
steps: 230 loss: 0.1357620805501938 lr: 0.006180904522613066 accuracy: 1.0
steps: 240 loss: 0.5153148174285889 lr: 0.0060134003350083755 accuracy: 0.875
steps: 250 loss: 0.42887353897094727 lr: 0.005845896147403685 accuracy: 0.875
steps: 260 loss: 0.3042680323123932 lr: 0.005678391959798995 accuracy: 0.8125
steps: 270 loss: 0.5260005593299866 lr: 0.005510887772194305 accuracy: 0.875
steps: 280 loss: 0.08018901199102402 lr: 0.005343383584589615 accuracy: 0.9375
steps: 290 loss: 0.14019827544689178 lr: 0.0051758793969849245 accuracy: 0.9375
steps: 300 loss: 0.06537951529026031 lr: 0.005008375209380235 accuracy: 1.0
steps: 310 loss: 0.706668496131897 lr: 0.004840871021775545 accuracy: 0.75
steps: 320 loss: 0.046374961733818054 lr: 0.004673366834170855 accuracy: 1.0
steps: 330 loss: 0.3127271831035614 lr: 0.0045058626465661646 accuracy: 0.875
steps: 340 loss: 0.4731540083885193 lr: 0.0043383584589614735 accuracy: 0.875
steps: 350 loss: 0.4876406490802765 lr: 0.004170854271356784 accuracy: 0.75
steps: 360 loss: 0.57007896900177 lr: 0.004003350083752094 accuracy: 0.75
steps: 370 loss: 0.6871400475502014 lr: 0.0038358458961474037 accuracy: 0.75
steps: 380 loss: 0.4668954908847809 lr: 0.0036683417085427135 accuracy: 0.875
steps: 390 loss: 0.3290950655937195 lr: 0.0035008375209380233 accuracy: 0.8125
steps: 400 loss: 0.2054392397403717 lr: 0.003333333333333333 accuracy: 0.875
steps: 410 loss: 0.16407813131809235 lr: 0.003165829145728643 accuracy: 0.9375
steps: 420 loss: 0.13321591913700104 lr: 0.002998324958123953 accuracy: 0.9375
steps: 430 loss: 0.25577545166015625 lr: 0.002830820770519263 accuracy: 0.875
steps: 440 loss: 0.40148788690567017 lr: 0.0026633165829145727 accuracy: 0.75
steps: 450 loss: 0.47690054774284363 lr: 0.0024958123953098825 accuracy: 0.75
steps: 460 loss: 0.1761317253112793 lr: 0.0023283082077051927 accuracy: 0.9375
steps: 470 loss: 0.07031602412462234 lr: 0.0021608040201005025 accuracy: 1.0
steps: 480 loss: 0.11138499528169632 lr: 0.0019932998324958123 accuracy: 1.0
steps: 490 loss: 0.10881906747817993 lr: 0.0018257956448911223 accuracy: 1.0
steps: 500 loss: 0.2125333696603775 lr: 0.0016582914572864321 accuracy: 0.9375
steps: 510 loss: 0.1883714199066162 lr: 0.0014907872696817421 accuracy: 0.875
steps: 520 loss: 0.08775968104600906 lr: 0.001323283082077052 accuracy: 1.0
steps: 530 loss: 0.05122673138976097 lr: 0.001155778894472362 accuracy: 1.0
steps: 540 loss: 0.14849573373794556 lr: 0.0009882747068676717 accuracy: 0.9375
steps: 550 loss: 0.0833907201886177 lr: 0.0008207705192629816 accuracy: 0.9375
steps: 560 loss: 0.28355494141578674 lr: 0.0006532663316582915 accuracy: 0.875
steps: 570 loss: 0.13419026136398315 lr: 0.00048576214405360134 accuracy: 1.0
steps: 580 loss: 0.12551864981651306 lr: 0.00031825795644891124 accuracy: 1.0
steps: 590 loss: 0.5847806930541992 lr: 0.00015075376884422112 accuracy: 0.8125

从训练过程来看,至少训练是有效的。

模型评价

现在要对模型进行评价,测试其在测试集上的准确率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
test_set = Dataset('test')
test_load = torch.utils.data.DataLoader(dataset=test_set,batch_size=32,collate_fn=collate_fn,shuffle=True,drop_last=True)
def Test():

model.eval()
correct = 0
total = 0

for i,(input_ids,attention_mask,token_type_ids,labels) in enumerate(test_load):
print(i)
with torch.no_grad():
out = model(input_ids,attention_mask,token_type_ids)

out = out.argmax(dim=1)
correct += (out == labels).sum().item()
total += len(labels)
print(correct/total)

Test()
torch.save(model.state_dict(),'./ChnSentiCorp.pth')#保存模型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
0.9197635135135135

在测试集上有91.98%的准确率,这个正确率不算高,但起码证明训练效果是有的。我们只是训练的一轮,如果再多训练几轮,说不定可以提高一些。并且下游模型只有一层网络,只提取的第一个字符,如果模型更复杂一些,说不定也能提高准确率。