xiangtingsl 发表于 2022-6-8 09:21

智能分诊项目总结

项目概述和目标

病人看病除了去医院挂号找相应科目的医生,现在还可以通过互联网医生服务在线问诊,我们知道的有“好大夫”、"京东健康"等。如果在病人不清楚自身的病灶,通过文字描述病症,在线问诊可能需要转接多次才能分配到合适的科室。因此,实现一个智能分诊系统,自动根据病人的描述精确识别病因,有助于提高科室分配的准确度,增加问诊效率。
本项目实现的智能分诊项目,属于文本多分类任务,是 NLP 领域最经典的任务之一。
数据描述

项目采用的数据集来自京东健康的部分公开数据,一共有 28000 多条样本,每条样本包含病情描述和相应的标签(科室)。文本已经做了初步处理:包括分词、标点符号替换、数字替换等。标签为 11 个不同的科室。



原始数据集

项目流程

一、项目总体框架




思维导图

二、项目代码文件


[*]data 文件夹:包含数据集 train.csv, eval.csv, test.csv,停用词表 stopwords.txt,标签映射表 label2idx.json
[*]model 文件夹:用于保存训练好的模型
[*]config.py:项目配置文件
[*]data.py:将原始数据集转换为 .csv 格式
[*]embedding.py:训练 tfidf、word2vec、fasttext 并保存特征
[*]features.py:用于特征工程
[*]model_ML.py:用于机器学习模型训练
[*]model_DL.py: 用于深度学习模型训练
三、具体流程

步骤1:数据预处理

去除停用词 在中文文本任务中,停用词通常包括英文字符、数字、数学符号、标点符号以及使用频率很高的汉字或词语。一方面这些词在文本中使用频率很高,占据很大的存储空间。另一方面这些词与文本所要表达内容的相关程度很低,没有什么实际含义。因此文本处理时首先将停用词去掉。
目前使用频率比较高的中文文本停用词表有中文停用词表 cn_stopwords、哈工大停用词表 hit_stopwords、百度停用词表 baidu_stopwords。本项目采用哈工大停用词表进行过滤。
代码:
# 读取停用表
stopWords =
# 遍历每个文本样本,文本的所有词之间用空格隔开
text = data['text'].apply(lambda x: " ".join())
步骤2:特征工程

本项目将使用 tfidf、word2vec、lda 以及人工抽取的一些特征做特征工程。训练词向量时,将训练集、验证集和测试集拼接在一起训练,获取词向量。
代码:
data_train = pd.read_csv(config.train_data_file, sep='\t').fillna("")
data_dev = pd.read_csv(config.eval_data_file, sep='\t').fillna("")
data_test = pd.read_csv(config.test_data_file, sep='\t').fillna("")
data = pd.concat()
1. TF-IDF 特征

TF-IDF 全称 Term Frequency - Inverse Document Frequency,是一种统计方法,评估某个词在文章中的重要程度。如果某个词在文章中出现频率很高,并且在语料库中的其他文章很少出现,则认为这个词对于这篇文章具有较高的表征能力。
TF 为词频,表示词在文本中出现的频率,用归一化形式来表示,即词频除以文本总词数。公式表示为:
https://www.zhihu.com/equation?tex=TF_%7Bi%2Cj%7D+%3D+%5Cfrac%7Bn_%7Bi%2Cj%7D%7D%7B%5Csum_kn_%7Bk%2Cj%7D%7D+%5C%5C
其中,https://www.zhihu.com/equation?tex=n_%7Bi%2Cj%7D 表示词条在文档 https://www.zhihu.com/equation?tex=d_j 中出现的次数。
IDF 为逆文件频率,表示词在其他文件中出现的频率,由文档总数除以包含该词的文档的数量,再求对数得出。用公式表示为:
https://www.zhihu.com/equation?tex=IDF_%7Bi%7D+%3D+log%5Cfrac%7B%7CD%7C%7D%7B1+%2B+%7Cj%3At_i%E2%88%88d_j%7C%7D+%5C%5C
其中,|D| 表示文档总数,https://www.zhihu.com/equation?tex=%7Cj%3At_i%E2%88%88d_j%7C 表示包含词条的文档数量,增加 1 是防止分母为0。
那么 TF-IDF 分数计算方法为:
https://www.zhihu.com/equation?tex=TF-IDF+%3D+TF+%5Ctimes+IDF+%5C%5C
TF-IDF 的特征可以通过 sklearn 库的 TfidfVectorizer 对象来训练。
代码:
# 实例化 TF-IDF 对象
count_vect = TfidfVectorizer(max_df=0.4,   # 词典忽略频率比阈值高的文档
                           min_df=0.001,   # 词典忽略频率比阈值低的文档
                           ngram_range=(1, 2) # n-gram 的上下边界)
# 训练 TF-IDF 模型
self.tfidf = count_vect.fit(self.train)

def get_tfidf(tfidf, data):
    # 将训练数据转化为 tfidf 分数
    data_tfidf = pd.DataFrame(tfidf.transform(text.tolist()).toarray())
    # tfidf 每一列重命名
    data_tfidf.columns = ['tfidf' + str(i) for i in range(data_tfidf.shape)]
    # 将 tfidf 特征合并到 data
    data = pd.concat(, axis=1)
构建的 tfidf 特征数量一个有2568个,如下所示:



tf-idf 特征向量

2. 基于词向量的特征

词向量即将词映射到实数向量的方法,将高维度的 one-hot 表示转化为低维度的连续值,使向量更加稠密。典型的词向量模型代表是 word2vec,由 CBOW(周围词预测中心词)和 Skip-gram(中心词预测周围词)两种方法训练。而 Fasttext 模型是改良版的 word2vec,具有更高效的训练速度,适用于大型数据集。
word2vec 模型可以使用 Gensim 库训练 。
代码:
# 实例化 word2vec 对象:
self.w2v = gensim.models.Word2Vec(min_count=2,# 忽略出现频率低的词
                                  window=5,   # 滑动窗口大小
                                  size=300,   # 词向量大小
                                  sample=6e-5,# 负采样的阈值
                                  alpha=0.03,# 学习率
                                  min_alpha=0.0007, # 最小学习率
                                  negative=15,# 负采样单词数
                                  iter=30,   # 训练次数
                                  max_vocab_size=50000) # 词表大小
# 构建词表
self.w2v.build_vocab(self.train)

# 训练词向量模型
self.w2v.train(sentences=self.train, total_examples=self.w2v.corpus_count, epochs=15)
构建词向量特征工程:

[*]基于 Word2vec 或者 Fasttext 求出词向量的最大值和平均值表示:
def wam(sentence, w2v_model, method='mean'):
    # 获取句子中的词向量,放入list中
    arr = np.array([])
    arr =
    # 第一种方法对一条样本中的词求平均
    if method == 'mean':
      return np.mean(np.array(arr), axis=0)
    # 第二种方法返回一条样本中的最大值
    elif method == 'max':
      return np.max(np.array(arr), axis=0)

[*]基于相似度计算求出词与标签结合后的词向量表示:
def Find_Label_embedding(example_matrix, label_embedding, method='mean'):
    # 根据矩阵乘法来计算label与word之间的相似度 cosin similiarity
    similarity_matrix = np.dot(example_matrix, label_embedding.T) / (np.linalg.norm(example_matrix) * (np.linalg.norm(label_embedding)))

    # 然后对相似矩阵进行均值池化,得到了“标签-词语”的注意力机制
    # 这里可以使用max-pooling和mean-pooling,然后取softmax
    attention = similarity_matrix.max(axis=1)
    attention = softmax(attention).reshape(-1,1)

    # 将样本的词嵌入与注意力机制相乘得到
    attention_embedding = example_matrix * attention
    if method == 'mean':
      return np.mean(attention_embedding, axis=0)
    else:
      return np.max(attention_embedding, axis=0)

[*]用大小分别为 2/3/4 的滑动窗口生成词向量的最大值和平均值表示: 对于分类模型,贡献最大的不一定是整个句子,可能是一部分单词、短语,因此采用滑动窗口抽取几个单词的词向量有助于提取更有效的特征。
def Find_embedding_with_windows(embedding_matrix, window_size=2,
                              method='mean'):
    # 初始化一个词向量的存储列表
    result_list = []
    # 遍历input的长度, 根据窗口的大小获取embedding,进行mean操作,然后将得到的结果extend到list中,最后进行mean/max聚合
    for k1 in range(len(embedding_matrix)):
      # 定义边界条件,如果当前位置+窗口大小超过input的长度,则取当前位置到结尾
      # mean 操作后要reshape为(1,300大小
      if int(k1 + window_size) > len(embedding_matrix):
            result_list.extend(np.mean(embedding_matrix, axis=0).reshape(1, 300))
      else:
            result_list.extend(np.mean(embedding_matrix, axis=0).reshape(1, 300))

    if method == 'mean':
      return np.mean(result_list, axis=0)
    else:
      return np.max(result_list, axis=0)

[*]汇总所有的特征
def generate_feature(sentence, embedding_model, label_embedding):
    # 获取embedding特征,不进行聚合
    w2v = wam(sentence, embedding_model, aggregate=False)#
# 生成word和label的联合embedding特征
    w2v_label_mean = Find_Label_embedding(w2v, label_embedding, method='mean')
    w2v_label_max = Find_Label_embedding(w2v, label_embedding, method='max')

    # 将embedding进行max, mean聚合
    w2v_mean = np.mean(np.array(w2v), axis=0)
    w2v_max = np.max(np.array(w2v), axis=0)

    # 滑窗处理embedding,然后聚合
    w2v_2_mean = Find_embedding_with_windows(w2v, 2, method='mean')
    w2v_3_mean = Find_embedding_with_windows(w2v, 3, method='mean')
    w2v_4_mean = Find_embedding_with_windows(w2v, 4, method='mean')
    w2v_2_max = Find_embedding_with_windows(w2v, 2, method='max')
    w2v_3_max = Find_embedding_with_windows(w2v, 3, method='max')
    w2v_4_max = Find_embedding_with_windows(w2v, 4, method='max')

    return {
      'w2v_label_mean': w2v_label_mean,
      'w2v_label_max': w2v_label_max,
      'w2v_mean': w2v_mean,
      'w2v_max': w2v_max,
      'w2v_2_mean': w2v_2_mean,
      'w2v_3_mean': w2v_3_mean,
      'w2v_4_mean': w2v_4_mean,
      'w2v_2_max': w2v_2_max,
      'w2v_3_max': w2v_3_max,
      'w2v_4_max': w2v_4_max
    }

[*]最后将所有特征合并
def get_embedding_feature(data, embedding_model):
    labelToIndex = label2idx(data)
    w2v_label_embedding = np.array([
      np.mean([
            embedding_model.wv.get_vector(word) for word in key
            if word in embedding_model.wv.vocab.keys()
      ],
                axis=0) for key in labelToIndex
    ])
    # 保存word和label的联合embedding
    joblib.dump(w2v_label_embedding, './data/w2v_label_embedding.pkl')
    # 根据未聚合的embedding数据,获取各类embedding特征
    tmp = data['text'].apply(lambda x: pd.Series(
      generate_feature(x, embedding_model, w2v_label_embedding)))
    tmp = pd.concat(, axis=1)
    data = pd.concat(, axis=1)
    return data
构建的 word2vec 特征向量如下,每个特征为 300 维:



word2vec 特征向量

3. 提取 LDA 特征

LDA (Latent Diriclet Allocation) 是一种主题模型,基于词袋模型方法。LDA 模型认为一篇文章是由一组词构成的一个集合,与词的顺序无关。LDA 模型可用于推测文档的主题分布,从而进一步做主题聚类或文本分类。
LDA 模型可以通过 Gensim 库来训练:
# 首先根据训练数据构建id和词的映射表
self.id2word = gensim.corpora.Dictionary(self.train)
# 采用doc2bow方法将每个样本变为词袋列表,每个单词表示为(id, 词频),从而构建好语料库
corpus =
# 建立LDA模型并训练
self.LDAmodel = models.LdaModel(corpus=corpus,   # 语料库
                              num_topics=30,   # 提取的主题数
                              id2word=self.id2word) # id与单词的映射表
构造 LDA 特征函数,返回每个主题的重要性概率分布
def get_lda_features_helper(lda_model, document):
    # 基于bag of word格式数据获取lda的特征
    topic_importances = lda_model.get_document_topics(document, minimum_probability=0)
    topic_importances = np.array(topic_importances)
    return topic_importances[:, 1]
提取 LDA 特征
def get_lda_features(data, LDAmodel):
    if isinstance(data.iloc['text'], str):
      data['text'] = data['text'].apply(lambda x: x.split())
    # 将每个文本转化为词袋向量
    data['bow'] = data['text'].apply(
      lambda x: LDAmodel.id2word.doc2bow(x))
    # 提取LDA特征
    data['lda'] = list(
      map(lambda doc: get_lda_features_helper(LDAmodel, doc), data['bow']))
    cols = ]
    return pd.concat(, array2df(data, 'lda')], axis=1)
提取 LDA 特征向量如下,共生成30个主题特征,每个主题分数越接近1,主题匹配度越高。



LDA 特征向量

4. 基于人工定义的特征

人工定义的特征可以包含以下几种:

[*]样本中单词的词性、个数及占比
[*]命名实体识别
[*]特殊符号个号,如感叹号、问号、标点符号总数
[*]唯一词的个数和占比
[*]每个句子词的平均个数
词性标注特征函数
def tag_part_of_speech(data):
    # 获取文本的词性(采用jieba库)
    # 格式为(单词,词性)
    words =
    # 计算名词个数
    noun_count = len( in ('NN', 'NNP', 'NNPS', 'NNS')])
    # 计算形容词个数
    adjective_count = len( in ('JJ', 'JJR', 'JJS')])
    # 计算动词个数
    verb_count = len( in ('VB', 'VBD', 'VBG', 'VBN', 'VBP', 'VBZ')
    ])
    return noun_count, adjective_count, verb_count
人工定义特征函数
def get_basic_feature_helper(text):
    text = text.split()
    # 分词
    queryCut = for i in text]
    # 词的个数
    num_words = len(queryCut)
    # 感叹号的个数
    num_exclamation_marks = queryCut.count('!')
    # 问号个数
    num_question_marks = queryCut.count('?')
    # 标点符号总数
    num_punctuation = sum(queryCut.count(w) for w in string.punctuation)
    # *&$%字符的个数
    num_symbols = sum(queryCut.count(w) for w in '*&$%')
    # 唯一词的个数
    num_unique_words = len(set(w for w in queryCut))
    # 唯一词与总词数的比例
    words_vs_unique = num_unique_words / num_words
    # 获取名词,形容词,动词的个数,使用tag_part_of_speech函数
    nouns, adjectives, verbs = tag_part_of_speech("".join(text))
    # 名词占词的个数的比率
    nouns_vs_length = nouns / num_words
    # 形容词占词的个数的比率
    adjectives_vs_length = adjectives / num_words
    # 动词占词的个数的比率
    verbs_vs_length = verbs / num_words
    # 平均词的个数
    mean_word_len = np.mean()
    return {...}
对每个样本的文本提取人工特征
def get_basic_feature(data):
    tmp = data['text'].apply(
      lambda x: pd.Series(get_basic_feature_helper(x)))
    return pd.concat(, axis=1)
构建人工特征向量如下:



人工特征向量

至此,整个特征工程构建完成。
步骤3:建立baseline模型训练

本项目采用机器学习模型 GBDT ,这是一种使用多个弱分类器(决策树)迭代训练的模型。而 LightGBM 是实现 GBDT 的一种框架,采用了带深度限制的 Leaf-wise 算法,可支持高效的并行训练,适用于大规模数据集,且只占用小的内存。
本项目采用多分类任务的方法,使用 LGBMClassifier 分类器。对于分类任务有几种典型评价指标可选:

[*]Accuracy:准确率。被正确预测的数量占所有数据的比例。优点是简单直观,要求数据集中正例和负例的样本是平衡的。 https://www.zhihu.com/equation?tex=Accuracy+%3D+%5Cfrac%7BTP+%2B+TN%7D%7BTP+%2B+FP+%2B+TN+%2B+FN%7D
[*]Precision: 查准率。预测结果为正例的样本里,真实情况也是正例的比例。不要求把所有的正例都能挑出来,但只要选中的,都尽可能使正例。如果选中了负例,代价会非常高。因此查准率是求精准。 https://www.zhihu.com/equation?tex=Precision+%3D+%5Cfrac%7BTP%7D%7BTP+%2B+FP%7D
[*]Recall:查全率。真实情况为正例的所有样本中,预测结果也是正例的比例。查全率与查准率刚好相反,要求尽可能求全,把所有的正例都找出来,宁可选错也不能漏掉。如果漏掉了正例,代价会非常高。 https://www.zhihu.com/equation?tex=Recall+%3D+%5Cfrac%7BTP%7D%7BTP+%2B+FN%7D
[*]F(β) Score:加权调和平均。由于 Precision 和 Recall 是相互对立的,其中一个分数很高的时候,另一个分数必要很低。如果同时兼顾 Precision 和 Recall,则需要用 F(β) 指标对二者进行加权调和平均。如果 β = 1,则平衡了 Precision 和 Recall 的权重;如果 β = 2,则 Recall 权重更大;如果 β = 0.5,则 Precision 权重更大。 https://www.zhihu.com/equation?tex=F_%CE%B2+%3D+%281+%2B+%CE%B2%5E2%29+%5Ctimes+%5Cfrac%7BPrecision+%5Ctimes+Recall%7D%7B%28%CE%B2%5E2+%5Ctimes+Precision%29+%2B+Recall%7D
[*]AUC Score:AUC (Area Under Curve) 是指 ROC 曲线以下的面积。ROC (Receiver Operating Characteristic) 曲线每个点反映对同一信号刺激的感受,其横轴为 FPR(假正类率或特异度),纵坐标为 TPR(真正类率或灵敏度)。原则上希望 TPR 越高且 FPR 越低,因此两个坐标存在一个中间最优的情况,通常 ROC 曲线在 y = x 这条直线的上方,即 AUC 取值在 0.5 ~ 1 之间,AUC 值越大模型性能越好。
使用 LGBM 模型训练代码:
# 初始化分类器
lgb_params = {
    'n_jobs':-1, #多线程并行运算的核的数量
    'num_leaves':30, #数的最大叶子数,控制模型复杂程度的关键参数
    'max_depth':5, #决策树的最大深度
    'learning_rate':0.1, #学习率
    'n_estimators':500, #弱学习器的数量,可理解为训练的轮数
    'reg_alpha':0, #L1正则化系数
    'reg_lambda':1, #L2正则化系数
    'objective':'multiclass', #任务目标
    'metric': 'multi_logloss', #模型度量标准
    'num_class':num_labels, #类别数
    'device':'gpu', #模型运算设备
    'early_stopping_round':500, #提前终止训练的轮数
    'verbosity':-1, #打印训练信息
    'random_state':42 # 随机种子数
}
self.clf = lgb.LGBMClassifier(**lgb_params)
# 训练模型的同时计算验证集的分数
self.clf.fit(X=X_train, y=y_train, eval_set=[(X_eval, y_eval)], eval_metric='logloss')
# 在测试集上预测
test_prediction = self.clf.predict(X_test)
# 计算并打印训练集和测试集分数
self.print_score(y_train, train_prediction, y_test, test_prediction)
Baseline 模型性能如下:
DatasetAccuracy ScoreTrain set0.976Test set0.825步骤4:模型优化

机器学习中的优化有模型参数优化和数据优化两个思路。模型参数优化包括网格搜索、随机搜索、遗传算法、粒子群优化、贝叶斯优化等方法,数据优化主要是处理样本不平衡。本工程采用网格搜索、贝叶斯优化和样本不平衡处理方法进行优化并进行结果对比。
优化1:GridSearchCV 网格搜索

GridSearchCV 是一种自动调参方法,分为网格搜索和交叉验证两个步骤。网格搜索是在指定参数范围中,得到参数的所有组合并逐个训练模型,从所有参数中找到验证集精度最高参数组合,是一个训练和比较的过程。



GridSearchCV 方法整体流程

交叉验证是指对数据集分成 K 个 Fold,并训练 K 次,每次训练选择其中一个 Fold 作为验证集,剩余 K-1 部分作为训练集。最后取这 K 次训练的平均分数作为结果。由于计算量较大,这种方法一般用在规模不是太大的数据集上。



交叉验证原理(图中以5个Fold为例)

由于网格搜索需要逐个参数组合进行运算,计算资源有限,只取部分参数。训练代码如下:
def Grid_Train_model(model, train_data, train_label):
    parameters = {'max_depth': ,
                  'learning_rate': ,
                  'n_estimators': ,
                  'subsample': ,
                  'colsample_bytree': ,
                  'reg_alpha': ,
               }

    fit_params = {"eval_set": []}
    gsearch = GridSearchCV(
             model,
                            param_grid=parameters,
                            scoring='accuracy',
                            cv=2,
                            verbose=4,
          )
    gsearch.fit(train_data, train_label, **fit_params)
    print("best parameters are {}".format(gsearch.best_params_))
    return gsearch

gsearch = self.Grid_Train_model(self.clf, X_train, y_train)
train_prediction = gsearch.predict(X_train)
test_prediction = gsearch.predict(X_test)
self.print_score(y_train, train_prediction, y_test, test_prediction)
网格搜索优化参数:

[*]colsample_bytree: 0.9
[*]learning_rate: 0.03
[*]max_depth: 5
[*]n_estimators: 500
[*]reg_alpha: 5
[*]subsample: 0.6
优化参数下模型性能:
DatasetAccuracy ScoreTrain set0.984Test set0.831优化2:贝叶斯优化

贝叶斯优化算法是根据一组采样点的函数值,预测任意点处的函数值的概率分布。相对于网格搜索每次只进行独立搜索的做法,贝叶斯优化使用已经搜索过的点的信息提高搜索速度和质量。其核心由两部分组成:1.高斯过程回归,先计算每一点处函数值的均值和方差;2. 再根据均值和方差构造采集函数,进而决定下一个采样点。
如下曲线为贝叶斯优化的实现,首先均匀选出 n(0) 个初始点,每次寻找下一个点时,根据已有的点建立高斯回归模型,得到任意点出函数值的后验概率,并生成采集函数。采集函数对于已有的采样的点采集函数值很小,在置信区间更宽的点采集函数值大,在函数均值大的点处采集函数值大。找到的函数极大值作为下一个搜索点。算法最后返回 N 个候选解的极大值作为最优解。



贝叶斯优化原理

使用 BayesSearchCV 的代码实现:
def BayesOptimize_Train_model(self, model, train_data, train_label):
    search_space = {
                  # 定义搜索空间
                  # Real定义实数范围,采用uniform/log-uniform的采样空间
                  # Integer定义整数范围,采用uniform/log-uniform的采样空间
                  'learning_rate': Real(0.01, 1.0, 'log-uniform'),
                  'max_depth': Integer(3, 10),
                  'num_leaves': Integer(2, 100),
                  'max_bin': Integer(5, 200),
                  'min_child_samples': Integer(20, 100),
                  'subsample': Real(0.1, 1.0, 'uniform'),
                  'subsample_freq': Integer(1, 50, 'uniform'),
                  'colsample_bytree': Real(0.1, 1.0, 'uniform'),
                  'min_split_gain': Real(0, 1.0, 'uniform'),
                  'subsample_for_bin': Integer(10000, 20000),
                  'reg_lambda': Real(0, 10, 'uniform'),
                  'reg_alpha': Real(0, 10, 'uniform'),
                  'n_estimators': Integer(10, 1000),
                  }

    bayes_cv_tuner = BayesSearchCV(estimator=model,
                                 search_spaces=search_space,
                                 cv=KFold(n_splits=2),
                                 n_iter=30,
                                 verbose=1,
                                 refit=True
                                 )

    bayes_cv_tuner.fit(train_data, train_label)
    return bayes_cv_tuner
贝叶斯搜索优化参数:

[*]colsample_bytree: 0.1
[*]learning_rate: 0.085
[*]max_depth: 6
[*]num_leaves: 86
[*]n_estimators: 819
[*]reg_lambda: 5.5
[*]subsample: 0.74
优化参数下模型性能:
DatasetAccuracy ScoreTrain set0.989Test set0.831优化3:样本不平衡处理

不同类别的样本量差异大,或少量样本代表了业务的关键数据,属于样本不平衡的情况,要求对少量样本的模式有很好的学习。工程方法中,通常从三个方面处理样本不均衡:1、数据相关处理:欠采样和过采样;2、模型相关处理:引入有倚重的模型算法,针对少量样本着重拟合,提升对少量样本特征的学习;3、评价指标相关处理:如 G-mean/MacroP/MicroP等。以下采用数据相关方法处理样本不平衡。
从样本数据分布可以明显看到绝大部分样本集中在其中一个类别中,其余类别所占比例很低:



原始数据分布

方法1:上采样(过采样),属于样本层面的方法。通过增加分类中少数样本的数量实现样本均衡,通常做法是在少数样本中加入随机噪声、干扰数据或通过一定规则产生新的合成样本。
本项目采用 SMOTE 方法 (Synthetic Minority Oversampling TEchnique),其基本思想是对少数类别样本进行分析模拟,将人工模拟的新样本添加到数据集中。具体实现先找出一个正样本(少数),采用 KNN 算法找到该正样本的 K 个邻近,并随机从 K 个邻近中选出一个样本,最后再正样本和选中的随机样本之间连线上,随机选取一个点,作为人工合成的新正样本。合成后所有类别样本数量相当。



SMOTE 算法原理

SMOTE 方法代码如下:
from imblearn.over_sampling import SMOTE
print("Start over-sampling")
X_train, y_train = SMOTE().fit_resample(X_train, y_train)
经 SMOTE 算法处理后的数据分布如下:


采用 Baseline 模型参数训练,性能如下:
DatasetAccuracy ScoreTrain set0.981Test set0.832方法2:下采样(欠采样),属于样本层面的方法。通过减少分类中多数样本的数量实现样本均衡,通常做法是对样本进行聚类。
本项目采用 imblearn 的 ClusterCentroids 算法,本质上是一种原型生成 (Prototype generation) 的方法,对多数类样本采用 K-means 算法生成 N 个簇样本中心,并且通过样本中心合成新的样本,替代原始样本。新的多数类样本数将与少数类样本数达到平衡。



ClsterCentroids 算法原理

ClusterCentroids 方法代码如下:
from imblearn.under_sampling import ClusterCentroids
print("Start under-sampling")
cc = ClusterCentroids()
X_train, y_train = cc.fit_resample(X_train, y_train)
经 ClusterCentroids 算法处理后的数据分布如下:


采用 Baseline 模型参数训练,性能如下:
DatasetAccuracy ScoreTrain set0.991Test set0.738方法3:过采样和欠采样结合。由于过采样容易导致过拟合,欠采样容易丢失有效信息,因此结合两种方法进一步削弱二者的缺点。这里采样 SMOTEENN 算法,先采用 SMOTE 算法进行过采样,再采用欠采样保留能够体现各类别特征的样本。其中 ENN (Edited Nearest Neighbours) 算法是基于 K 最近邻算法编辑数据集,找出与邻居不友好的样本然后移除,从而实现数据平衡。
SMOTEEN 方法代码如下:
from imblearn.combine import SMOTEENN
print("Start over-sampling and under-sampling")
sme = SMOTEENN()
X_train, y_train = sme.fit_resample(X_train, y_train)
经 SMOTEENN 算法处理后的数据分布如下:


采用 Baseline 模型参数训练,性能如下:
DatasetAccuracy ScoreTrain set0.950Test set0.790优化4:采用深度学习模型

前面的方法采用的都是机器学习模型,机器学习方法需要对输入内容做具体特征工程。而深度学习方法将输入文本映射成高维的词向量,并通过深度学习模型从词向量中自动提取特征。这里采用 huggingface 预训练的 bert-base-chinese 模型进行训练,训练轮数为 2 个 epoch 时模型性能如下:
DatasetAccuracy ScoreTrain set0.885Test set0.873项目结论

本项目的主要尝试是采用机器学习方法做文本分类,在特征工程的构造上,提取了 tf-idf、word2vec、lda 以及人工构建的特征,并采用了 GBDT 模型进行训练。从 Baseline 模型的性能来看测试集预测精度为 0.825,是一个还算不错的表现。
后续采取了多种优化手段,首先是模型训练参数的优化,分别使用网格搜索和贝叶斯优化进行尝试,在测试集上的预测精度都提升到了 0.831,但贝叶斯优化的只使用了 20 次模型迭代就达到了这个性能,而网格搜索由于人为设定了可选参数,尝试了 100 多个参数组合才达到同样的性能。因此贝叶斯优化在效率上远高于网格搜索方法。另外是从数据层面进行优化,主要是对数据分布不平衡进行了处理。原始数据集中男科类别占了绝大多数,其余科目类别只占少数。具体方法分别使用了上采样、下采样和上下混合采样方式进行处理,使各类别样本数据量相当。从训练结果来看,上采样方法效果更好,模型预测精度提升到了 0.832。
最后使用了深度模型学习进行性能对比。尽管不需要做特征工程,但是深度学习模型依靠复杂的模型结构和庞大的模型参数,自动提取各维度的特征,最后模型的预测精度达到 0.873,超过了上述所有机器学习模型。深度学习模型更适用于文本这种非结构化数据的处理。
后续进一步改进,可以考虑采用不同的机器学习模型训练并进行融合、尝试更多的特征、用深度学习模型先对样本预训练再fine-tune等方法。
项目代码

Github:https://github.com/Kelvin-Ho/Intelligence_Triage
参考


[*]Book: Introduction to Machine Learning with Python
[*]Hugging Face 官网
[*]Scikit learn 官网
页: [1]
查看完整版本: 智能分诊项目总结