Kaggle首战记录(2)-English Language Learning-baseline的数据处理

这部分是和baseline有关的数据处理环节。

EDA

和统计一样,上来得先做一个数据探索EDA。

瞄一下数据,大概是成正态分布,这比较符合常识,因此可以说不太存在数据不平衡的现象。

注意这里的数据不平衡是指训练集和真实分布的差距,而不是score值的相互比较。

数据共3911行,说实话不多,因此也比较依赖之后的数据增强。

baseline打算用roberta-base做预训练层,roberta的预训练任务token数都是不超过512的,因此EDA也要关注数据过roberta的tokenizer后的情况。

导入数据:

1
2
3
4
5
6
7
8
path = '../../../mydata/ka/ell/train.csv'

import pandas as pd
data = pd.read_csv(path)
data['full_text'] = data['full_text'].apply(lambda x: x.strip()) #简单地去除一下头尾

from transformers import RobertaTokenizer
tokenizer = RobertaTokenizer.from_pretrained('../../../mydata/roberta-base/')

token数

然后把文本列用tokenizer映射一下,关注token数。

1
2
3
4
5
6
7
8
token_len_df = data['full_text'].map(lambda x : tokenizer(x, return_tensors='pt')["input_ids"].shape[1])

print(token_len_df.max())
print(token_len_df.min())
print(token_len_df.mean())
print(token_len_df.median())
print(token_len_df.count())
print(token_len_df[token_len_df > 512].count())

结果如下:

1
2
3
4
5
6
1457
28
493.9312196369215
463.0
3911
1555

可以看到最长的已经到1457了,而且大于512的有接近一半了,因此考虑切割。

段落数

切割怎么切好呢?段落一般是文意的分割点,因此我们再EDA一下段落:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_paragraph_count(x): #计算段落数量,其实后来想想直接用正则表达式对文本操作不是更方便(
sen_tensor = tokenizer(x, return_tensors='pt')["input_ids"][0]
count = 0
for token in sen_tensor:
if token.item() == 50118:
count += 1
return count / 2 + 1

paragraph_count_df = data['full_text'].map(get_paragraph_count)

print(paragraph_count_df.max())
print(paragraph_count_df.min())
print(paragraph_count_df.mean())
print(paragraph_count_df.median())
print(paragraph_count_df[paragraph_count_df > 30].count())

结果如下:

1
2
3
4
5
52.0
1.0
5.538097673229353
5.0
5

也就是有的居然达到了52段!实在是丧心病狂,看一下原文:

1
print(data['full_text'][paragraph_count_df.idxmax()])

输出如下,只能说很会玩:

本来我还进行了段落中最大token数的探索,但现在感觉没必要,无论探索结果如何,你都不可能仅仅把段落数作为唯一切割依据。

数据处理

数据处理的方法就呼之欲出了,对于一条长文本,先转化为token,然后不断二分切割,当然这个二分只是“类”二分,最好按段落切割,其次的分割标准是句号。然后我发现有的作文连句号都没有,因此还加上了逗号和空格。

处理过程

代码如下:

是用递归实现的。

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
def txt_to_tensor(x):
x = x.strip()
sen_tensor = tokenizer(x, return_tensors='pt', padding="max_length", max_length=512)["input_ids"] #padding补齐,不然没办法cat
if sen_tensor.shape[1] <= 512: #如果本段落合格了,则返回
return sen_tensor
str_len_mid = len(x) // 2 #句子中央
about_mid = 0
try:
about_mid = str_len_mid + x[str_len_mid:].index('\n') #否则尝试在句子中央之后找一找换行符
except:
try:
about_mid = str_len_mid - x[str_len_mid::-1].index('\n') #如果找不到,在句子中央之前找一找换行符
except:
try:
about_mid = str_len_mid + x[str_len_mid:].index('.') #再找不到,找找句号
x1 = txt_to_tensor(x[:about_mid])
x2 = txt_to_tensor(x[about_mid+1:])
return torch.cat((x1, x2))
except:
about_mid = str_len_mid + x[str_len_mid:].index(',') #逗号
x1 = txt_to_tensor(x[:about_mid])
x2 = txt_to_tensor(x[about_mid+1:])
return torch.cat((x1, x2))
if about_mid > str_len_mid * 1.5 or about_mid < str_len_mid * 0.5: #尽量分割合理,如果两边长度差别太大,试试用句号分割
try:
about_mid2 = str_len_mid + x[str_len_mid:].index('.')
if abs(about_mid2 - str_len_mid) < abs(about_mid - str_len_mid):
about_mid = about_mid2
except:
pass
x1 = txt_to_tensor(x[:about_mid]) #再递归探索
x2 = txt_to_tensor(x[about_mid+1:])
return torch.cat((x1, x2))

这个处理方法是有问题的

首先,分割其实可以不用那么合理,毕竟连28个token的文章都有。

其次,对于不同的评分维度,数据处理的要求是不同的,本方法的目的是尽可能地保存“段落”、“章节”,而像语法句法、短语词汇这些可能根本不需要保留这些信息,但在baseline六个维度一起训练的条件下这还是有必要的。

处理方法的问题也给数据增强带来思路(虽然这不叫增强了叫查漏补缺)

不管那么多了,我们apply一下,再看看我们处理后每个数据的子句多少:

1
2
3
4
5
data['full_tensor'] = data['full_text'].apply(txt_to_tensor)

d = data['full_tensor'].map(lambda x: x.shape[0])
print(d.max())
print(d[d>1].count())

输出如下:

1
2
5
1555

也就是最多有5个子句,多于1个子句的有1555个,和上面大于512token的数量一样。

数据集创建

写一个十折交叉检验的数据集:

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
class writing_dataset(Dataset):
def __init__(self, data=data, ki = 0, typ='train'):
self.x = []
self.y = [] #六维的评分
self.k = 10 #10折交叉
self.xy = []
for dat in data.iterrows():
self.x.append(dat[1]['full_tensor'])
self.y.append(torch.tensor([dat[1]['cohesion'], dat[1]['syntax'], dat[1]['vocabulary'], dat[1]['phraseology'], dat[1]['grammar'], dat[1]['conventions']]))
self.length = len(self.y)
self.xy.extend((self.x[i], self.y[i]) for i in range(self.length))
random.seed(1)
random.shuffle(self.xy)
self.my_xy = []
every_z_len = self.length // self.k
if typ == 'val':
self.my_xy = self.xy[every_z_len * ki : every_z_len * (ki+1)]
elif typ == 'train':
self.my_xy = self.xy[: every_z_len * ki] + self.xy[every_z_len * (ki+1) :]
else:
print('Wrong type!')


def __getitem__(self, index):
return self.my_xy[index]

def __len__(self):
return len(self.my_xy)

平平无奇的写法,注意数据集的自变量是二维的tensor,tensor的第一个维度(子句数)是不一样的,所以在dataloader的时候要写一个collate函数处理一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def collate(batches):
max_sub_count = 0
first = True
x = None
y = None
for batch in batches:
max_sub_count = max(max_sub_count, batch[0].shape[0]) #统计batch内的最大子句数
for batch in batches:
if first:
first = False
x = batch[0]
y = batch[1].unsqueeze(0) #batch[1]的维度是[6],这里变成了[1, 6]才好cat
else:
x = torch.cat((x, batch[0]))
y = torch.cat((y, batch[1].unsqueeze(0)))

need_sub_count = max_sub_count - batch[0].shape[0]

if need_sub_count:
x = torch.cat((x, torch.repeat_interleave(batch[0][0].unsqueeze(0), need_sub_count, dim=0))) #没到最大子句的,取第一个子句来补充,注意,这导致了我们池化层必须是maxpool,或者反过来maxpool下我们才能这么做。

return x, y

数据处理就到这里。


Kaggle首战记录(2)-English Language Learning-baseline的数据处理
https://bebr2.com/2022/09/11/Kaggle首战记录(2)-English Language Learning-baseline的数据处理/
作者
BeBr2
发布于
2022年9月11日
许可协议