Skip to content

以Pipeline的方式构建NLP任务,包括文本清洗、关键词提取、特征抽取、文本分类等任务

License

Notifications You must be signed in to change notification settings

zhulei227/pipenlp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pipenlp

介绍

pipenlp包可以方便地将NLP任务构建为Pipline任务流,目前主要包含的功能有:

  • 数据清洗,关键词提取等:pipenlp.preprocessing
  • 文本特征提取,包括bow,tfidf等传统模型;lda,lsi等主题模型;fastext,word2vec等词向量模型;pca,nmf等特征降维模型:pipenlp.representation
  • 文本分类,包括lgbm决策树、logistic回归、svm等传统机器学习模型:pipenlp.classification

安装

pip install git+https://github.com/zhulei227/pipenlp

git clone https://github.com/zhulei227/pipenlp.git
cd pipenlp
python setup.py install

使用

导入PipeNLP主程序

from pipenlp import PipeNLP

准备pandas.DataFrame格式的数据

import pandas as pd
data=pd.read_csv("./data/demo.csv")
data.head(5)
text label
0 动力差 消极
1 油耗很低,操控比较好。第二箱油还没有跑完。油耗显示为5.9了,本人13年12月刚拿的本,跑出... 积极
2 乘坐舒适性 积极
3 最满意的不止一点:1、车内空间一流,前后排均满足使用需求,后备箱空间相当大;2、外观时尚,珠... 积极
4 空间大,相对来说舒适性较好,性比价好些。 积极

数据清洗

from pipenlp.preprocessing import *
nlp=PipeNLP()
nlp.pipe(RemoveDigits())\
   .pipe(RemovePunctuation())\
   .pipe(RemoveWhitespace())

data["output"]=nlp.fit(data["text"]).transform(data["text"])
data[["output"]].head(5)
output
0 动力差
1 油耗很低操控比较好第二箱油还没有跑完油耗显示为了本人年月刚拿的本跑出这样的油耗很满意了
2 乘坐舒适性
3 最满意的不止一点车内空间一流前后排均满足使用需求后备箱空间相当大外观时尚珠光白尤其喜欢看起来...
4 空间大相对来说舒适性较好性比价好些

分词

默认空格表示分割

nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())

data["output"]=nlp.fit(data["text"]).transform(data["text"]).head(5)
data[["output"]].head(5)
output
0 动力 差
1 油耗 很 低 操控 比较 好 第二 箱油 还 没有 跑 完 油耗 显示 为了 本人 年 月 ...
2 乘坐 舒适性
3 最 满意 的 不止 一点 车 内 空间 一流 前后排 均 满足 使用 需求 后备箱 空间 相...
4 空间 大 相对来说 舒适性 较 好性 比价 好些

文本特征提取

BOW词袋模型

from pipenlp.representation import *
nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(BagOfWords())

nlp.fit(data["text"]).transform(data["text"]).head(5)
一下 一个 一个劲 一个月 一个舒服 一二 一些 一体 一停 ... 黄色 黑屏 黑底 黑烟 黑白 黑色 默认 鼓包 齐全
0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 3865 columns

LDA主题模型

nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(LdaTopicModel(num_topics=10))

nlp.fit(data["text"]).transform(data["text"]).head(5)
0 1 2 3 4 5 6 7 8 9
0 0.033340 0.699931 0.033340 0.033339 0.033345 0.033339 0.033336 0.033347 0.033343 0.033338
1 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.970954 0.000000 0.000000
2 0.033336 0.033340 0.699950 0.033337 0.033336 0.033341 0.033336 0.033347 0.033336 0.033342
3 0.000000 0.000000 0.000000 0.000000 0.000000 0.978558 0.000000 0.000000 0.000000 0.000000
4 0.011114 0.011114 0.899973 0.011115 0.011114 0.011114 0.011114 0.011114 0.011114 0.011114

Word2Vec模型

nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(Word2VecModel(embedding_size=8))

nlp.fit(data["text"]).transform(data["text"]).head(5)
0 1 2 3 4 5 6 7
0 -0.661387 -0.205723 0.258319 0.536984 0.449666 0.042449 1.769084 0.686457
1 -0.643488 -0.274599 0.326613 0.609752 0.457684 -0.092639 1.953447 0.827511
2 -0.279103 -0.064463 0.215476 0.347268 0.171509 -0.069043 0.954895 0.398952
3 -0.541652 -0.224891 0.234527 0.451804 0.338954 -0.057171 1.587017 0.670156
4 -0.476938 -0.239309 0.246525 0.455723 0.267393 -0.052079 1.387308 0.579978

FastText模型

nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(Word2VecModel(embedding_size=8))

nlp.fit(data["text"]).transform(data["text"]).head(5)
0 1 2 3 4 5 6 7
0 -0.604970 -0.282136 0.254526 0.583515 0.509314 0.033652 1.754926 0.643264
1 -0.583390 -0.364780 0.325500 0.668123 0.530914 -0.105864 1.951512 0.782000
2 -0.248199 -0.099972 0.212815 0.366762 0.198890 -0.073193 0.935753 0.373854
3 -0.493239 -0.295528 0.233387 0.496731 0.396241 -0.067167 1.582285 0.633446
4 -0.431377 -0.301842 0.244642 0.493892 0.317640 -0.061102 1.376675 0.545068

PCA降维

nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(BagOfWords())\
   .pipe(PCADecomposition(n_components=2))

nlp.fit(data["text"]).transform(data["text"]).head(5)
0 1
0 -1.257324 -0.222434
1 1.071957 0.266467
2 -1.288506 -0.212718
3 0.281789 -0.626062
4 -1.293974 -0.353279

文本分类

LGBM

from pipenlp.classification import *
nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(BagOfWords())\
   .pipe(LGBMClassification(y=data["label"]))

nlp.fit(data["text"]).transform(data["text"]).head(5)
积极 消极
0 0.245708 0.754292
1 0.913772 0.086228
2 0.435600 0.564400
3 0.999868 0.000132
4 0.916361 0.083639

Logistic回归

from pipenlp.classification import *
nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(BagOfWords())\
   .pipe(PCADecomposition(n_components=8))\
   .pipe(LogisticRegressionClassification(y=data["label"])) 

nlp.fit(data["text"]).transform(data["text"]).head(5)
积极 消极
0 0.507574 0.492426
1 0.806157 0.193843
2 0.449968 0.550032
3 0.999967 0.000034
4 0.780798 0.219202

模型持久化

保存

nlp.save("nlp.pkl")

加载

由于只保留了模型参数,所以需要重新声明模型结构信息(参数无需传入)

nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(BagOfWords())\
   .pipe(PCADecomposition())\
   .pipe(LogisticRegressionClassification())
<pipenlp.pipenlp.PipeNLP at 0x20ce742a188>
nlp.load("nlp.pkl")
nlp.transform(data["text"]).head(5)
积极 消极
0 0.507574 0.492426
1 0.806157 0.193843
2 0.449968 0.550032
3 0.999967 0.000034
4 0.780798 0.219202

高阶用法

自定义pipe模块

自需要实现一个包含了fit,transform,get_params和set_params这四个函数的类即可:

  • fit:用于拟合模块中参数
  • transform:利用fit阶段拟合的参数去转换数据
  • get_params/set_params:主要用于模型的持久化

比如如下实现了一个对逐列归一化的模块

from pipenlp.base import PipeObject
import numpy as np
import scipy.stats as ss
class Normalization(PipeObject):
    def __init__(self, normal_range=100, normal_type="cdf", std_range=10):
        PipeObject.__init__(self)
        self.normal_range = normal_range
        self.normal_type = normal_type
        self.std_range = std_range
        self.mean_std = dict()

    def fit(self, s):
        if self.normal_type == "cdf":
            for col in s.columns:
                col_value = s[col]
                mean = np.median(col_value)
                std = np.std(col_value) * self.std_range
                self.mean_std[col] = (mean, std)
        self.input_col_names = s.columns.tolist()
        return self

    def transform(self, s):
        s_ = copy.copy(s)
        if self.normal_type == "cdf":
            for col in s_.columns:
                if col in self.mean_std:
                    s_[col] = np.round(
                        ss.norm.cdf((s_[col] - self.mean_std[col][0]) / self.mean_std[col][1]) * self.normal_range, 2)
        elif self.normal_type == "range":
            for col in s_.columns:
                if col in self.mean_std:
                    s_[col] = self.normal_range * s_[col]
        self.output_col_names = s_.columns.tolist()
        return s_

    def get_params(self):
        params = PipeObject.get_params(self)
        params.update({"mean_std": self.mean_std, "normal_range": self.normal_range, "normal_type": self.normal_type})
        return params

    def set_params(self, params):
        PipeObject.set_params(self, params)
        self.mean_std = params["mean_std"]
        self.normal_range = params["normal_range"]
        self.normal_type = params["normal_type"]
nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(BagOfWords())\
   .pipe(PCADecomposition(n_components=8))\
   .pipe(LogisticRegressionClassification(y=data["label"]))\
   .pipe(Normalization())

nlp.fit(data["text"]).transform(data["text"]).head(5)
积极 消极
0 50.44 49.56
1 53.94 46.06
2 49.74 50.26
3 56.22 43.78
4 53.58 46.42

pipenlp的拆分与合并

PipeNLP的pipe对象也可以是一个PipeNLP,所以我们以将过长的pipeline拆分为多个pipeline,分别fit后再进行组合,避免后面流程的pipe模块更新又要重新fit前面的pipe模块

nlp1=PipeNLP()
nlp1.pipe(ExtractChineseWords())\
    .pipe(ExtractJieBaWords())\
    .pipe(BagOfWords())\
    .pipe(PCADecomposition(n_components=8))

pca_df=nlp1.fit(data["text"]).transform(data["text"])
pca_df.head(5)
0 1 2 3 4 5 6 7
0 -1.257324 -0.222444 -0.217656 -0.118004 -0.204318 -0.156744 -0.009275 0.088116
1 1.071957 0.266230 1.563062 0.070491 -1.205419 0.808515 -0.552603 -0.990944
2 -1.288506 -0.212731 -0.270413 -0.095124 -0.098725 -0.014406 -0.003268 0.106673
3 0.281789 -0.626053 2.274090 0.573257 1.360892 -0.242684 -0.069548 0.656460
4 -1.293974 -0.353290 0.239912 0.013726 0.844865 -0.353885 0.112731 -0.263181
nlp2=PipeNLP()
nlp2.pipe(LogisticRegressionClassification(y=data["label"]))\
    .pipe(Normalization())

nlp2.fit(pca_df).transform(pca_df).head(5)
积极 消极
0 50.45 49.55
1 53.90 46.10
2 49.74 50.26
3 56.27 43.73
4 53.67 46.33
nlp_combine=PipeNLP()
nlp_combine.pipe(nlp1).pipe(nlp2)

nlp_combine.transform(data["text"]).head(5)
积极 消极
0 50.45 49.55
1 53.90 46.10
2 49.74 50.26
3 56.27 43.73
4 53.67 46.33

持久化

nlp_combine.save("nlp_combine.pkl")
nlp1=PipeNLP()
nlp1.pipe(ExtractChineseWords())\
    .pipe(ExtractJieBaWords())\
    .pipe(BagOfWords())\
    .pipe(PCADecomposition())

nlp2=PipeNLP()
nlp2.pipe(LogisticRegressionClassification())\
    .pipe(Normalization())

nlp_combine=PipeNLP()
nlp_combine.pipe(nlp1).pipe(nlp2)
nlp_combine.load("nlp_combine.pkl")
nlp_combine.transform(data["text"]).head(5)
积极 消极
0 50.45 49.55
1 53.90 46.10
2 49.74 50.26
3 56.27 43.73
4 53.67 46.33

部署生产环境

transform_single

通常,在生产线上使用pandas效率并不高,且生产的输入格式通常是字典格式(json),所以如果需要部署生产,我们需要额外添加一个函数:

  • transform_single:实现与transform一致的功能,而input和output需要修改为字典格式
nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(BagOfWords())\
   .pipe(PCADecomposition(n_components=2))\
   .pipe(LGBMClassification(y=data["label"]))
nlp.fit(data["text"])
<pipenlp.pipenlp.PipeNLP at 0x2319c46a1c8>
nlp.transform_single({"text":"空间大,相对来说舒适性较好,性比价好些。"})
{'积极': 0.8888528928971476, '消极': 0.11114710710285232}

自定义pipe模块

接着为前面的Normalization类再加一个transform_single函数

class NormalizationExtend(Normalization):
    def transform_single(self, s):
        s_ = copy.copy(s)
        if self.normal_type == "cdf":
            for col in s_.keys():
                if col in self.mean_std:
                    s_[col] = np.round(
                        ss.norm.cdf((s_[col] - self.mean_std[col][0]) / self.mean_std[col][1]) * self.normal_range, 2)
        elif self.normal_type == "range":
            for col in s_.keys():
                if col in self.mean_std:
                    s_[col] = self.normal_range * s_[col]
        return s_
nlp=PipeNLP()
nlp.pipe(ExtractChineseWords())\
   .pipe(ExtractJieBaWords())\
   .pipe(FastTextModel())\
   .pipe(LGBMClassification(y=data["label"]))\
   .pipe(NormalizationExtend())
nlp.fit(data["text"])
<pipenlp.pipenlp.PipeNLP at 0x231996dad08>
nlp.transform_single({"text":"空间大,相对来说舒适性较好,性比价好些。"})
{'积极': 54.18, '消极': 45.82}

自动化测试

部署生产环境之前,我们通常要关注两点:

  • 离线训练模型和在线预测模型的一致性,即tranform和transform_single的一致性;
  • transform_single对当条数据的预测性能

这些可以通过调用如下函数,进行自动化测试:

  • auto_check_transform:只要有打印[success],则表示一致性测试通过,性能测试表示为[*]毫秒/每条数据,如果有异常则会直接抛出,并中断后续pipe模块的测试
nlp.auto_check_transform(data["text"])
(<class 'pipenlp.preprocessing.ExtractChineseWords'>)  module transform check [success], single transform speed:[0.01]ms/it
(<class 'pipenlp.preprocessing.ExtractJieBaWords'>)  module transform check [success], single transform speed:[0.17]ms/it
(<class 'pipenlp.representation.FastTextModel'>)  module transform check [success], single transform speed:[2.09]ms/it
(<class 'pipenlp.classification.LGBMClassification'>)  module transform check [success], single transform speed:[1.76]ms/it
(<class '__main__.NormalizationExtend'>)  module transform check [success], single transform speed:[0.16]ms/it

TODO

  • 加入TextCNN、HAT、LSTM+Attention、Bert更高阶模型
  • 支持预训练词向量

About

以Pipeline的方式构建NLP任务,包括文本清洗、关键词提取、特征抽取、文本分类等任务

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages