考虑未定义类型的多分类

考虑这样一个场景,我们要处理一个多分类问题,由于目标的空间是开放的,我们无法穷举所有的类别。目前我们制定了K类去训练一个多分类模型,在预测未知数据时,有可能出现未识别的类型,此时我们的多分类模型会赋予它k类中的一类。然鹅,这个赋予是错误的。那怎样去避免和处理这样的现象呢?

方案思考

遇到这个问题,很多人的第一直观想法是,分别给没类训练一个one-class classifier(比如one-class svm)不就好了。

这确实是一个看起来很不错的想法。但是one-class classifer本质上是做异常检测的,如果某一类的样本空间分布很分散,并没有聚成团,此时可能正确的样本都会被判断成错误的类别。实际中遇到各种乱七八糟的数据,我对one-class classifier的性能是打问号的。当然如果能精心设计,以上当我扯淡。

除了one-class classifer外,我粗略考虑到的两种稍微靠谱一点的做法,这里做一下记录。(貌似找到了这方面的paper,比如Unseen Class Discovery in Open-world Classification,之后找时间再写一篇学习下这类做法)

  1. 将multi-class改成multi-label任务
  2. 设计一个pipline,第一个分类器判断是否见过,第二就是常规的多分类

第二个做法中第一个分类器的设计,见仁见智。可以设计成one-class分类器,也可以计算与已知样本/类别的距离等这样简单的做法。

目前实践了第一种做法,做一下记录

multi-label

Multi-label指的是,一个样本可能有多个标签,不再是单一标签。比如一副海滩的图片,我们对其进行分类,其可能同时具备标签,白云、大海、沙滩、山、树。将其只分给其中一个比如大海,是不合适的。

扯个题外话,分类问题我觉得可以分为四类

  1. Binary classification
  2. Multi-class classification
  3. Multi-label classification
  4. Multi-target classification

提供两个工具:scikit-multilearnmeka

Multi-label算法的核心其实是想利用label和label之间的相关性,我们这里处理未见类别其实没有利用到这个思想。

具体算法可以分为如下几类

  1. Binary Relevance:每个标签看作一个单独的类分类问题。
  2. Classifier Chain:单独分类每个类别,但是前面得分类伪标签会作为后面分类的特征。(利用相关性,需要组合不同的链)
  3. Label Powerset:组合类别做多个多分类问题
  4. Adaptive:这类就比较丰富了,MLKNN、神经网络、集成学习

在本次考虑的场景下,我们并没有考虑类别之间的关系,所以采用BR做一个测试,本次没有使用scikit-multilearn,仅仅用了sklearn的multi-label工具。

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import ShuffleSplit, train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.metrics import classification_report
from sklearn.preprocessing import LabelBinarizer, LabelEncoder
from sklearn.multiclass import OneVsRestClassifier
import numpy as np

def train_one_vs_rest(self):
    all_data_file = 'data.txt'
    data_all, labels_all = self.process_data(all_data_file)
    # lb = LabelEncoder()
    lb = LabelBinarizer()
    y = lb.fit_transform(labels_all)
    print(lb.classes_)
    text_train, text_test, y_train, y_test = \
        train_test_split(data_all, y, test_size=0.33, random_state=0)
    count_vect = CountVectorizer()
    train_vec = count_vect.fit_transform(text_train)
    clf = OneVsRestClassifier(LogisticRegression())
    clf.fit(train_vec, y_train)
    print(len(clf.estimators_))
    test_vec = count_vect.transform(text_test)
    pred = clf.predict(test_vec)
    print(classification_report(y_test, pred))
    self.model = clf
    self.vectorizer = count_vect
    pred = self.pred_multi_label(test_vec)
    print(pred)
    
def pred_multi_label(self, vectors):
    preds = []
    for i in range(len(self.model.estimators_)):
        preds.append(
            self.model.estimators_[i].predict_proba(vectors)[:, 1])
    return np.column_stack(tuple(preds))

这里需要注意的是,在预测过程中,如果采用OneVsRestClassifier默认的predictproba,得到的没类的概率是归一化的。我想得到的就是具体属于每一类的概率,不要归一化。所以,这里采用了其属性`estimators`来计算具体的概率。

实验发现,效果贼好。对于已知类别,对应类上的概率基本大于80%,而对于未见类别,最高也在57%左右。

此时我们完全可以采用70%去截断,如果一个类别未达到,那就是未知类别了。

当然,这种做法还有一些问题,接下来梳理下更好的做法。