问题描述: 笔者在文本分类场景中使用了roberta+pool+dense的三分类模型。采用预训练模型做项目的时候经常苦于数据太少,模型泛化性差,因此收集了1300W数据。在我尝试暴力出奇迹的时候,遇到了部分问题,在此记录一下。

一. 数据预处理时间长

尽管数据预处理步骤比较简单,一般也就清洗、分词、token2id等操作(我一般把token2id放在预处理阶段做),但是由于数据量比较大时,也很耗时间。

1.分词提速

jieba分词开启并行很简单,一行代码开启

jieba.enable_parallel(100)

但是这里注意,jieba并行的过程是一个 jieba.cut()这一动作中的,如果输入的是多个句子,则jieba会并行处理多个句子。

# 错误jieba.cut('我爱北京天安门。')jieba.cut('北京是中国首都。')#正确示范jieba.cut('我爱北京天安门。\n北京是中国首都。')

因此如果需要采用并行,需要先包装多行数据,再拆分出来。具体参考

temp_lines = texts[:100]_res = ' '.join((jieba.cut('\n'.join(temp_lines))))split_words = [_.lstrip(' ').rstrip(' ') for _ in _res.split('\n')]res.extend(split_words)
2. 函数并行

当然,使用预训练模型不需要对中文进行分词,因此此处耗时主要在数据清洗,正则处理等操作上,所以方法1已经不起作用啦。过程就是先读取文件到内存,然后分多个进程预处理,最终获取所有进程处理结果,写入文件。

import multiprocessingimport mathdef func(id, texts):    print('enter %d' % (id))    res = []    for i, text in enumerate(texts):        if i % 10000 == 0 and i != 0:            print(i, '/', id)        value=tokenizer.encode(first=text, max_len=seq_len)[0]        res.append(value)    print('leave %d' % (id))    return id, res    def quick_func(texts, func):    pool = multiprocessing.Pool(processes=CPUS)    results = []    for i in range(CPUS):        imin = i*math.ceil(len(texts)/CPUS)        imax = min((i+1)*math.ceil(len(texts)/CPUS), len(texts))        results.append(pool.apply_async(func, (i, texts[imin:imax],)))    pool.close()    pool.join()    print("Sub-process(es) done.")        res = []    for _ in results:        res.append(_.get())    res = sorted(res, key=lambda x: x[0])    texts = []    for _ in res:        texts.extend(_[1])    return texts

注意! func内部出错,子进程直接结束,不raise错误的。如果要调试,最好还是加上traceback查看问题

在func中完全可以使用jieba的cut,不需要开启jieba并行。在处理数据500万左右的时候,各个进程已经结束,但是该函数迟迟不返回值,直到我htop查看内存,发现内存快速增长到150G之后不在增长,程序也卡住啦,发现可能是 raw句子和处理后的结果占用内存过高,因此这个方法也不行了。

3. 文件并行

查看资料的时候发现,有老哥处理几亿行文件使用linux bash来预处理,速度明显提升,我也想尝试,但是最终由于功力不足,失败。折中办法,先将文件拆分成多个文件,python并行处理多个文件并输出结果到多个文件,然后合并多个文件到单个文件。(注意,这里会丢失各行顺序,如果去重需要在外部处理,仅仅是读取写入文件单线程也还是挺快的)

并行读取同一个文件或者并行写入同一个文件是危险的,可能会写入或者读取混乱(错误)

import multiprocessingCPUS = 40IN_DATA_SPLIT_PREFIX = 'data-split-tmp-in'OUT_DATA_SPLIT_PREFIX = 'data-split-tmp-out'seq_len = 256  # 文本最大长度vocab_path = '../../basedata/chinese_wwm_ext_L-12_H-768_A-12/vocab.txt'tokenizer = LimitBertTokenizer(vocab_path)  # text2 *4 < text1char_tool = CharTools()def _clean_tokenize(infile, outfile, filters=[]):    fin = open(infile, 'r')    fout = open(outfile, 'w')    for i, line in enumerate(fin):        if i % 10000 == 0 and i != 0:            print(i, ' / ', infile)        items = line.strip().split('\t')        ###########------------#########                if len(items) != 6:            continue        object_id, _, operator_id, title, content, action = items        type = 'model' if operator_id == '0' else 'human'        if type in filters:            continue        if action not in action2label.keys():            continue        label = action2label[action]        title = char_tool.clean(title)        content = char_tool.clean(content)        title = title[:seq_len]        content = content[:seq_len]        wordids, segmentids = tokenizer.encode(            first=content, second=title, max_len=seq_len)        fout.write(json.dumps(            {'type': type, 'label': label, 'wordids': wordids, 'segmentids': segmentids})+'\n')        ###########------------#########    fin.close()    fout.close()def parallel(_func, infile, outfile, filters=[]):    os.system('split -n l/%d %s %s' %              (CPUS, infile, IN_DATA_SPLIT_PREFIX))    print("split files done")    pool = multiprocessing.Pool(processes=CPUS)    for small_data_file_in in [_ for _ in os.listdir('.') if _.startswith(IN_DATA_SPLIT_PREFIX)]:        small_data_file_out = small_data_file_in.replace(            IN_DATA_SPLIT_PREFIX, OUT_DATA_SPLIT_PREFIX)        pool.apply_async(_func, args=(            small_data_file_in, small_data_file_out, filters,))    pool.close()    pool.join()    print("Sub-process(es) done.")    os.system('cat %s* > %s' % (OUT_DATA_SPLIT_PREFIX, outfile))    os.system('rm %s*' % (IN_DATA_SPLIT_PREFIX))    os.system('rm %s*' % (OUT_DATA_SPLIT_PREFIX))    print("done.")

二. numpy加载后占用内存太大

之前由于机器内存够用+数据量不算太大,在训练过程中我都是加载前文处理的json文件为numpy数据然后使用model.fit()进行训练的,代码如下

def load_from_json(filename):    labels = []    wordids = []    segmentids = []    with open(filename, 'r') as f:        for i, line in enumerate(f):            if i % 100000 == 0 and i != 0:                print('载入数据:%d ' % i)            item = json.loads(line.strip())            labels.append(item['label'])            wordids.append(item['wordids'])    wordids = np.array(wordids)    segmentids = np.zeros((len(labels), seq_len), int)    labels = tf.keras.utils.to_categorical(labels)    [train_wordids, val_wordids, train_segmentids, val_segmentids,     train_label3s, val_label3s] = train_test_split(wordids, segmentids, label3s, test_size=0.01, stratify=labels,random_state=0)    return [[train_wordids, train_segmentids],            [train_label3s],            [val_wordids, val_segmentids],            [val_label3s]]train_X,train_y,val_X,val_y=load_from_json()model.fit(train_X, train_Y,          validation_data=(val_X, val_Y),)

直到,我遇到了千万数据集,首先读取后占用机器超过150G内存,另外python提示单个变量占用超过10%内存,就此程序卡住,因此不得不更换方法。

  • tf.data: 官方推荐的方法,但是我感觉使用json或者re都不是很方便,加上tf.function写起来不是很方便,放弃。
  • data.generator:一般generator很常见,但是很多人使用的时候都是把数据完全读进内存,然后在generator中实现shuffle和输出batch的功能(就没有generator的作用啦),这里由于数据量太大,明显是不能读取所有数据进内存的。为了保持shuffle的功能,这里还是顺序读取文件,但是维持一个buffer, 在buffer中对数据进行shuffle。
class DataGenerator():    # 读取 generator    def __init__(self, json_file, batch_size=2, min_buffer_size=200000, max_buffer_size=300000, shuffle=True):        self.f = open(json_file, 'r')        self.batch_size = batch_size        file_len = int(os.popen('wc -l %s' % json_file).read().split()[0])        self.len = math.ceil(file_len / batch_size)        self.buffer_lines = []        self.max_buffer_size = max_buffer_size        self.min_buffer_size = min_buffer_size        self.shuffle = shuffle        self.check_load()    def __len__(self):        return self.len    def _read_line(self):        """获取一行数据"""        line = self.f.readline()        if not line:            self.f.seek(0)            line = self.f.readline()        return line    def check_load(self):        """保证buffer中的数据量满足要求"""        if len(self.buffer_lines) > self.min_buffer_size:            return        else:            while len(self.buffer_lines) <= self.max_buffer_size:                self.buffer_lines.append(self._read_line())                            if self.shuffle:                random.shuffle(self.buffer_lines)    def _handle(self, lines):        pass    def __iter__(self):        while True:            self.check_load()            lines, self.buffer_lines = self.buffer_lines[:self.batch_size], self.buffer_lines[self.batch_size:]            yield self._handle(lines)class MyDataGenerator(DataGenerator):    def _handle(self, lines):        word_ids, segment_ids, labels = [], [], []        for line in lines:            item = json.loads(line.strip())            labels.append(item['label'])            word_ids.append(item['wordids'])            segment_ids.append(item['segmentids'])        word_ids = np.array(word_ids)        segment_ids = np.array(segment_ids)        labels = tf.keras.utils.to_categorical(labels, num_classes=3)        return [word_ids, segment_ids], labelstrain_data = MyDataGenerator(params.tain_file, batch_size=params.finetune_batch_size*gpus,                             min_buffer_size=100000, max_buffer_size=300000)val_data = MyDataGenerator(params.val_file, batch_size=params.finetune_batch_size*gpus,                           min_buffer_size=0, max_buffer_size=100000, shuffle=False)model.fit(iter(train_data),          validation_data=iter(val_data),          steps_per_epoch=len(train_data),          validation_steps=len(val_data))

具体实现了一个DataGenerator父类,其必须包含数据条目(可返回batch个数),因为model.fit中需要指定其迭代次数。为了保证generaotr持续有输出,在读取文件到末尾的时候,自动返回文件头。另外由于是在buffer中shuffle,其不能保证文件中的各行只输出一次(但是能保证一个epoch最多max_buffer_size个重复的),需要依据数据条目酌情设置,这里应该优化,在达到文件末尾后等全量buffer清空后在seed到文件头。另外,实现了子类,具体实现lines到numpy的操作。其实也可以把数据预处理和token2id放在这里,但是每个epoch都要处理一次,有点浪费时间,因此习惯把所有预处理和toekn2id放到train前的预处理脚本中。

三. 模型训练速度慢,需要多卡训练

在tf2之后并行更简单了,代码如下:

import tensorflow as tffrom keras_bert import load_trained_model_from_checkpointdef create_model(bert_train=False):    bert = load_trained_model_from_checkpoint(        config_path, checkpoint_path,        training=False,        trainable=bert_train,        seq_len=SEQ_LEN,)    inputs = bert.inputs[:2]    dense = bert.get_layer('Encoder-12-FeedForward-Norm').output    dense = tf.keras.layers.Lambda(lambda x: x[:, 1:, :])(dense)    dense1 = tf.keras.layers.GlobalMaxPool1D()(dense)    dense2 = tf.keras.layers.GlobalAveragePooling1D()(dense)    dense = tf.keras.layers.Concatenate()([dense1, dense2])    dense = tf.keras.layers.Dense(params.dnn_units, activation='relu')(dense)    dense = tf.keras.layers.Dropout(rate=params.dropout)(dense)    output = tf.keras.layers.Dense(        units=3, activation='softmax', name='3cls')(dense)    model = tf.keras.models.Model(inputs,  output)    return modelos.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3,4,5,6,7"gpus = tf.config.experimental.list_physical_devices(device_type='GPU')for gpu in gpus:    tf.config.experimental.set_memory_growth(gpu, True)gpus = len(os.environ["CUDA_VISIBLE_DEVICES"].split(','))strategy = tf.distribute.MirroredStrategy()with strategy.scope():    model = create_model(bert_train=False)    scheduler = tf.keras.callbacks.ReduceLROnPlateau(        monitor='val_loss', factor=0.5, patience=int(params.fit_opt_patience), min_delta=1e-7)    loss = LossGenerate(params.model_loss)    metrics = ['accuracy']    optimizer = tf.keras.optimizers.Adam(params.fit_lr)    csvlogger = tf.keras.callbacks.CSVLogger(os.path.join(        params.model_dir, 'log.tsv'), append=True, separator='\t')    earlystop = tf.keras.callbacks.EarlyStopping(        monitor='val_loss', patience=params.fit_patience)    checkpoint = tf.keras.callbacks.ModelCheckpoint(filepath=os.path.join(params.model_dir, 'stage1.weight.h5'),                                                    save_weights_only=True, save_best_only=True)    model.compile(loss=loss, metrics=metrics,                  optimizer=optimizer)

只需要在strategy.scope()下定义模型就行,很简单。但是我也遇到一个问题: 在预测时,在strategy.scope()加载存储的模型文件报错:

from keras_bert import get_custom_objectscustom_objects = get_custom_objects()with strategy.scope():   model = tf.keras.models.load_model(            model_path, custom_objects=custom_objects)# 报错

具体错误google很久也没有结果,最终发现在strategy.scope下载入权重文件是可以的(可能是哪里实现兼容性不强吧),代码:

with strategy.scope():  model = create_model(bert_train=False)    model.load_weights(os.path.join(params.model_dir, 'stage1.weight.h5'))

实验结果

最终,在6卡v100并行下, 1000万长度384的分类模型训练好啦。stage1为固定bert训练结果, 01-0.4238为所有参数train的结果。发现了:1000W数据,max-len设置为384, RoBERTa-wwm-ext 模型训练需要接近25小时。其实还是蛮快的.... 另外: 大力出奇迹的模型效果还可以!!!

为了凑够1万字,放一下上文用到的LossGenerate函数

def LossGenerate(name='ce', *args, **kwargs):    NAMES = ('ce', 'focal', 'dmi')    kwargs = locals()['kwargs']    assert (name in NAMES), ' loss not defined!!!'    if name == 'ce':        return tf.keras.losses.CategoricalCrossentropy()    if name == 'focal':        gamma = kwargs.get('gamma', 2.)        alpha = kwargs.get('alpha', 0.25)        def categorical_focal_loss_fixed(y_true, y_pred):            y_pred /= K.sum(y_pred, axis=-1, keepdims=True)            epsilon = K.epsilon()            y_pred = K.clip(y_pred, epsilon, 1. - epsilon)            cross_entropy = -y_true * K.log(y_pred)            loss = alpha * K.pow(1 - y_pred, gamma) * cross_entropy            return K.mean(loss, axis=1)        return categorical_focal_loss_fixed    if name == 'dmi':        def dmi_loss(y_true, y_pred):            y_true = tf.transpose(y_true, perm=[1, 0])            mat = tf.matmul(y_true, y_pred)            loss = -1.0 * tf.math.log(tf.math.abs(tf.linalg.det(mat)) + 0.001)            return loss        return dmi_loss

for循环数据量太大_中文文本分类roberta大力出奇迹之数据量大的问题相关推荐

  1. python 文本分类卡方检验_中文文本分类:你需要了解的10项关键内容

    文本分类指的是计算机通过算法对输入的文本按照一定的类目体系进行自动化归类的过程.在人工智能浪潮席卷全球的今天,文本分类技术已经被广泛地应用在文本审核.广告过滤.情感分析和反黄识别等NLP领域.本文从达 ...

  2. 如何用 Python 和循环神经网络(RNN)做中文文本分类?

    本文为你展示,如何使用 fasttext 词嵌入预训练模型和循环神经网络(RNN), 在 Keras 深度学习框架上对中文评论信息进行情感分类. 疑问 回顾一下,之前咱们讲了很多关于中文文本分类的内容 ...

  3. textcnn文本词向量_基于Text-CNN模型的中文文本分类实战

    1 文本分类 文本分类是自然语言处理领域最活跃的研究方向之一,目前文本分类在工业界的应用场景非常普遍,从新闻的分类.商品评论信息的情感分类到微博信息打标签辅助推荐系统,了解文本分类技术是NLP初学者比 ...

  4. python文本分类模型_下载 | 最全中文文本分类模型库,上手即用

    原标题:下载 | 最全中文文本分类模型库,上手即用 本文转自『大数据文摘』 如何选择合适的模型上手进行中文文本分类呢? 别慌,福利来了,GitHub上一位名为"huwenxing" ...

  5. Tensorflow使用CNN卷积神经网络以及RNN(Lstm、Gru)循环神经网络进行中文文本分类

    Tensorflow使用CNN卷积神经网络以及RNN(Lstm.Gru)循环神经网络进行中文文本分类 本案例采用清华大学NLP组提供的THUCNews新闻文本分类数据集的一个子集进行训练和测试http ...

  6. 【NLP】中文文本分类数据增强方法:EDA 与代码实现

    数据增强可以算作是做深度学习算法的一个小trick.该介绍主要出自论文:EDA: Easy Data Augmentation Techniques for Boosting Performance ...

  7. python中文文本分析_基于CNN的中文文本分类算法(可应用于垃圾邮件过滤、情感分析等场景)...

    基于cnn的中文文本分类算法 简介 参考IMPLEMENTING A CNN FOR TEXT CLASSIFICATION IN TENSORFLOW实现的一个简单的卷积神经网络,用于中文文本分类任 ...

  8. Pytorch TextCNN实现中文文本分类(附完整训练代码)

    Pytorch TextCNN实现中文文本分类(附完整训练代码) 目录 Pytorch TextCNN实现中文文本分类(附完整训练代码) 一.项目介绍 二.中文文本数据集 (1)THUCNews文本数 ...

  9. python 中文文本分类

    写这篇博文用了很多时间和精力,如果这篇博文对你有帮助,希望您可以打赏给博主相国大人.哪怕只捐1毛钱,也是一种心意.通过这样的方式,也可以培养整个行业的知识产权意识.我可以和您建立更多的联系,并且在相关 ...

最新文章

  1. 剑指offer:面试题29. 顺时针打印矩阵
  2. 通过MATLAB读取mnist数据库
  3. git-flow 流程 备忘清单
  4. 运筹学广泛的使用计算机,运筹学笔记
  5. oracle 只对成绩前三名进行排序其余不变_2021年采用美术统考成绩的重点院校名单汇总...
  6. 从英伟达 vs ATI的芯片大战看GPU前世今生
  7. linux服务器程序开发,怎样搭建一个linux开发服务器
  8. SHTML+INC机制说明!
  9. java 网络字节序转主机字节序_C语言高级编程——网络编程技术
  10. 浅谈智能电能表的远程预付费 系统设计
  11. ios弱网测试_弱网测试方法整理
  12. ARP攻击实战以及防御手段
  13. 如何擦除Altera FPGA的配置器件EPCS中的内容
  14. 神舟电脑怎么重装系统 神舟电脑重装系统步骤
  15. 大众点评数据爬虫思路[更新版]
  16. 大厂Java岗春招必看:论一个面渣逆袭之路上必学得那些知识点
  17. 用 CSS3 做一个流星雨动画
  18. C语言基础 判断周几
  19. 爬虫是什么,该如何学习爬虫呢?
  20. maven 常用命令goal

热门文章

  1. UBOOT问题收集(1)--balignl 16, 0xdeadbeef
  2. 动静分离-静态资源缓存控制
  3. Android开发启动未注册的activity,Hook使用demo
  4. c语言编写期末考试成绩,C语言期末考试卷(A卷含答案).doc
  5. java 多线程跑数据_java——多线程的实现方式、三种办法解决线程赛跑、多线程数据同步(synchronized)、死锁...
  6. java实现文件在线预览
  7. 小程序怎么打出横线效果_成都小程序开发:哪些因素会影响小程序的运营效果?...
  8. 面向对象之编写一个完整的类
  9. 1011. World Cup Betting (20)
  10. 【C++深度剖析教程23】继承中的访问级别