NLP技术应用

实用技术

同义词替换

故障搜索领域,查询关键字往往有固定的模式,比如“某某指标差”、“某某速率低”,为一主谓结构,主语是名词,谓语是形容词或动词,且谓语为形容词时,都是“差”、“不好”、“不达标”这样的负面形容词。

现在有个诉求:如果搜索“XX指标差”时搜不出内容,能自动用其他的负面形容词做同义词替换,比如“XX指标不好”。

方案一:词性标注序列

用词性标注来做,我们可使用下面的正则式来匹配词性序列(词性前后加<>加以区分):

<v?n.*>(<d>)*(<a.*>|<v.*>)$

亦即查询关键字遵循:

名词+副词+[形容词或动词]

这么一种序列模式。

接下来要处理几点:

  1. 处理否定副词(例如“不”、“非”),程度副词(例如“太”、“非常”)不用处理
  2. 对作为谓语的形容词或动词,找出其同义词;特别的,若副词为否定副词,则要找“否定副词+谓语”的同义词
  3. 同义词替换即可。

上述过程中,我们需要准备:同义词词典+否定副词列表。同义词词典有实体书,针对特定领域(如故障查询)我们也可手写。

方案二:依存句法

基于依存句法分析,比如“XX速率不高”。

我们只需分析出核心词的SBV和ADV关系即可,后续操作同前。查找核心词的SBV和ADV代码如下(使用HanLP):


    public void testDependency()
    {
        CoNLLSentence dep = HanLP.parseDependency("XX速率不高");

        System.out.println(dep);

        CoNLLWord core = findHED(dep);
        CoNLLWord subject = findSBV(core, dep);
        CoNLLWord adv = findADV(core, dep);
        System.out.println("subject:" + (subject != null ? subject.NAME:null) + ",adv:" + (adv != null ? adv.NAME:null) + ",verb:" + core.NAME);
    }

    private CoNLLWord findHED(CoNLLSentence dep)
    {
        for (CoNLLWord w: dep.word)
        {
            if (w.DEPREL.equals("核心关系"))
            {
                return w;
            }
        }
        return null;
    }

    private CoNLLWord findSBV(CoNLLWord hed, CoNLLSentence dep)
    {
        for (CoNLLWord w: dep.word)
        {
            if (w.HEAD.equals(hed) && w.DEPREL.equals("主谓关系"))
            {
                System.out.println(w.NAME);
                return w;
            }
        }
        return null;
    }

    private CoNLLWord findADV(CoNLLWord hed, CoNLLSentence dep)
    {
        for (CoNLLWord w: dep.word)
        {
            if (w.HEAD.equals(hed) && w.DEPREL.equals("状中结构"))
            {
                return w;
            }
        }
        return null;
    }

文本分类

文本分类用途很广,比如垃圾邮件检测、文档划分等。

一般的,我们使用sklearn来做原型,选择效果最好的分类器,再用java来做版本实现。

sklearn的分类器遵循下面的接口:

#训练
fit(train_features, train_labels)

#预测
predict(test_features)

其中,train_features和test_features是二维特征矩阵,其中,行为采样数,列为特征数,矩阵元素类型为数值;train_labels为数组,数组长度为采样数,数组元素类型则没有限制,能区别分类结果即可(比如,对于二分类问题可定义为数字1/0或字符串Y/N)。

特征提取

对于文本分类,首先要做的是从文本中抽取出数值型的特征值,构成二维特征矩阵,这个过程也是文本向量化的过程,可以使用前面提到的词袋法。

词袋法

使用sklearn提供的CountVectorizer类,样例如下:

def bow_extracter(self, corpus):
        # ngram_range是说可以把几个词连起来视作一个特征,bow一般就是一个词一个特征,否则特征数会爆炸
        vectorizer = CountVectorizer(stop_words=None, ngram_range=(1,1), analyzer='word')
        features = vectorizer.fit_transform(corpus)
        feature_names = vectorizer.get_feature_names()
        trace('feature num:%d' % len(feature_names))        
        return vectorizer, features

提取的结果是语料中的每个词作为一个特征,语料中的每篇文档形成

[1,0,2,0...]

这样的向量。从前面词袋法的介绍得知,这里的特征数目可能会非常巨大(取决于词典里词的总数)。

CountVectorizer具体说明参看:

http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html#sklearn.feature_extraction.text.CountVectorizer.fit_transform

tf-idf特征提取

也可使用tf-idf方法完成文本向量化,该方法是在词袋法的基础上考虑了idf,即“逆向文档频率”,削弱了那些在多个文档中频繁出现的词的影响(这些词往往不那么重要,因为它们无法区分文档)。样例代码如下:

    def tfidf_extracter(self, corpus):
        # 做l2正则化,即均方。l1则为误差绝对值的和
        vectorizer = TfidfVectorizer(min_df=1,
                                 norm='l2',
                                 smooth_idf=True,
                                 use_idf=True,
                                 ngram_range=(1,1))   
        return self.extracter_i(vectorizer, corpus)    

    def extracter_i(self, vectorizer, corpus):
        features = vectorizer.fit_transform(corpus)
        feature_names = vectorizer.get_feature_names()
        trace('feature num:%d' % len(feature_names))
        trace('/ '.join(feature_names))
        # trace(features.toarray())
        return vectorizer, features
tf-idf介绍
tf-idf(d, t) = tf(t) * idf(d, t)

其中:
t为词,tf(t)为词频;
d为文档,idf(d, t)为逆文档频率。

idf(d, t)=log [ N / df(d, t) ] + 1

N为文档总数,df(d, t)为包含词t的文档数。可见,词t在文档里出现的越少,idf值越大,该词的影响力就越大。+1主要是确保那些在所有文档里出现的词(显然,此时df(d, t)=N,所以log [ N / df(d, t) ] = 0)不会完全被忽略。

为避免除数为0,sklearn的tf-idf向量化默认会做idf平滑,上述公式演变成:

idf(d, t) = log [ (1 + N) / (1 + df(d, t)) ] + 1
l1和l2正则化

l2正则化,即均方。l1则为误差绝对值的和。

下面是一个例子:

    from sklearn import preprocessing
    
    fea = [
    [1,0,-1],
    [1,1,1],
    ]

    # [[ 1/2=0.5         0.         -0.5       ]
    # [ 1/3=0.33333333  0.33333333  0.33333333]]
    X = preprocessing.normalize(fea, norm='l1', axis=1)
    print X

    # [[ 1/sart(2)=0.70710678  0.         -0.70710678]
    # [ 1/sqrt(3)=0.57735027  0.57735027  0.57735027]]
    X = preprocessing.normalize(fea, norm='l2', axis=1)
    print X

注意

特征向量正则化的方向有根据采样记录正则,此时axis=1,也可根据特征正则,此时axis=0。有时候,根据采样记录正则是有问题的,比如采样记录里有不同性质的特征,如电压、温度,强行正则没有意义。

doc2vec向量化

还可以使用前面提到的word2vec的改进版doc2vec来生成文档向量,相比于词袋法,doc2vec的特征数更可控。核心代码为:

    def extract_features(self, norm_corpus):
        model = gensim.models.Doc2Vec(norm_corpus,
                                       vector_size=500,
                                       dm=1,
                                       window=5,
                                       min_count=2)

        trace('before scale:%s' % model.docvecs[0])
        ##获得文档向量
        return model.docvecs
其他方法

我们也可以采用其他做法来获得特征值,例如:

  • 文档标题
  • 业务关键词打分
  • 长、短段落打分

模型训练及评估

模型训练使用前面提到的sklearn的fit方法。我们重点关注评估。

分类器效果评估有几种方法:

  • hold-out,将原始数据随机分为两组,一组做为训练集,一组做为验证集,利用训练集训练分类器,然后利用验证集验证模型,记录最后的分类准确率作为分类器的性能指标。
  • k-fold,将原始数据分成K组(一般是均分),将每个子集数据分别做一次验证集,其余的K-1组子集数据作为训练集,这样会得到K个模型。用这K个模型的验证集的分类准确率的平均数作为此K-CV下分类器的性能指标。K一般大于等于2

k-fold就是通常所谓的“交叉验证”(cross-validation)。

hold-out样例代码如下:

    def hold_out(self, corpus, labels, clfs, clf_names):
        train_corpus, test_corpus, train_labels, test_labels = self.split_data(corpus, labels)
        trace('split corpus complete')

        norm_train_corpus = self.normalize(train_corpus)
        norm_test_corpus = self.normalize(test_corpus)
        trace('normalize corpus complete')

        # 训练和测试必须使用相同的特征,所以此处获得测试特征用的是transform而非fit_transform
        bow_vectorizer, bow_train_features = self.bow_extracter(norm_train_corpus)
        bow_test_features = bow_vectorizer.transform(norm_test_corpus)
        
        for clf,cn in zip(clfs, clf_names):
            trace('classifier:%s' % cn)
            pred = self.train_and_classify(clf, bow_train_features, train_labels, bow_test_features, test_labels)
            
    def split_data(self, corpus, labels):
        #使用sklearn提供的train_test_split做训练集、验证集的划分
        X_train, X_test, y_train, y_test = train_test_split(corpus, labels, test_size=0.3, random_state=42)        
        return X_train, X_test, y_train, y_test
    
    def train_and_classify(self, clf, train_features, train_labels, test_features, test_labels):
        # 训练模型
        clf.fit(train_features, train_labels)
        # 预测
        predictions = clf.predict(test_features)
        # 将预测值与真实值做对比,生成混淆矩阵
        self.get_metrics(test_labels, predictions)
        return predictions
        
    def get_metrics(self, true_labels, predicted_labels):
        trace('准确率:%.2f' % np.round(
            metrics.accuracy_score(true_labels,
                                   predicted_labels),
            2))
        trace('精度:%.2f' % np.round(
            metrics.precision_score(true_labels,
                                    predicted_labels,
                                    average='weighted'),
            2))
        trace('召回率:%.2f' % np.round(
            metrics.recall_score(true_labels,
                                 predicted_labels,
                                 average='weighted'),
            2))
        trace('F1得分:%.2f' % np.round(
            metrics.f1_score(true_labels,
                             predicted_labels,
                             average='weighted'),
            2))          

这是k-fold的例子:

    def kfold(self, corpus, labels, clfs, clf_names):
        norm_corpus = self.normalize(corpus)
        _, bow_features = self.tfidf_extracter(norm_corpus)

        for clf, cn in zip(clfs, clf_names):
            trace('classifier:%s' % cn)
            #使用sklearn提供的cross_val_score自动做K-CV,这里我们使用F1作为评估指标
            trace(cross_val_score(clf, bow_features, labels, cv=5, scoring='f1'))

precision和recall

混淆矩阵(confusion matrix)定义如下,1代表正类,0代表负类:

预测1预测0
实际1True Positive(TP)False Negative(FN)
实际0False Positive(FP)True Negative(TN)

precision的公式是P=TP/(TP+FP)或 P=TN/TN+FN,对于正类别而言,表示预测为正的样本中有多少预测对了。precison是从预测结果来看的,表示通常意义上的分类准确率。

recall的公式是R=TP/(TP+FN)或R=TN/TN+FP ,对于正类别而言,表示实际为正的样本中有多少预测对了(即召回了多少正样本,剩下的正样本都丢失了)。recall则是从实际结果来看的。

文档聚类

文档聚类的思路是先将文档向量化(词袋法、doc2vec皆可),再利用KMean等聚类算法进行聚类。

聚类可以跟人工标注结合起来使用。

下面的例子使用doc2vec做文档向量化,再利用sklearn里的KMean算法做聚类:

class TextCluster(TextClassifier_d2v):
    """docstring for TextCluster"""
    def __init__(self):
        super(TextCluster, self).__init__()

    def run(self, work_dir):
        spam_path = '%s/data/spam_data.txt' % work_dir
        ham_path = '%s/data/ham_data.txt' % work_dir

        corpus, labels = self.load_raw_data(spam_path, ham_path)
        norm_corpus = self.normalize(corpus)
        features = self.extract_features(norm_corpus)
                   

        #设定簇数
        clf = KMeans(n_clusters=2)
        #模型训练
        s = clf.fit(features)
        

        #输出每个样本所属的簇,这里有0和1两个簇
        labels = clf.labels_.tolist()
        trace(labels)
        trace('cluster 0 count:%d, cluster 1 count:%d' % (labels.count(0), labels.count(1)))
        
        #用来评估簇的个数是否合适,距离越小说明簇分的越好,选取临界点的簇个数
        trace(clf.inertia_)

        trace(clf.predict([list(features[0])]))
    
    #用doc2vec将文档转成向量
    def extract_features(self, norm_corpus):
        model_path = 'd2v.model'
        if os.path.exists(model_path):
            model = gensim.models.Doc2Vec.load(model_path)
            trace('load d2v model success')
        else:    
            model = gensim.models.Doc2Vec(norm_corpus,
                                           vector_size=self.vec_size,
                                           dm=1,
                                           window=5,
                                           min_count=2)
            model.save(model_path)

        trace('doc vector size:%d' % len(model.docvecs))    
        trace('model first rec:%s, type:%s' % (model.docvecs[0], type(model.docvecs[0])))       
        return self.to_list(model.docvecs)  

    def to_list(self, features):
        new_features = []
        for i in xrange(0, len(features)):
            new_features.append(features[i])
        return new_features  
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值