这部分是和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" ] 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())
输出如下:
也就是最多有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 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 ]) for batch in batches: if first: first = False x = batch[0 ] y = batch[1 ].unsqueeze(0 ) 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 ))) return x, y
数据处理就到这里。