返回列表

#2 solution

485. NBME - Score Clinical Patient Notes | nbme-score-clinical-patient-notes

开始: 2022-02-01 结束: 2022-05-03 临床决策支持 数据算法赛
#2 解决方案

#2 解决方案

作者: CPMP | 排名: 第2名

首先,感谢 NBME 主办方和 Kaggle 提供了这个具有挑战性的比赛。我非常喜欢我们获得了比标记数据多 10 倍的未标记数据,以便尝试无监督或半监督技术。如果没有这些,比赛的趣味性会大打折扣。

让我描述一下我的解决方案。大部分代码可以在我的推理 notebook 中看到。由于某种原因,Kaggle 不允许我在这里放置链接,因为我已经在另一篇文章中分享了它。你可以从排行榜访问它。然后查找最新版本,我在公共数据集中分享了所有检查点。

数据处理

我很快发现标注不一致,就像大多数参与者一样。我的猜测是,标注者有时只识别了给定特征的某些出现情况,而不是全部。例如 Adderall(拼写?)的情况可以看出这一点。虽然在现实项目中修复这些标注很有意义,但在这里可能是个坏主意。确实,没有理由相信测试标注不像训练数据标注那样不一致。

我进一步假设标注者更有可能遗漏重复的标注而不是第一次出现的标注。这意味着标注具有序列依赖性,因此在某个阶段使用 RNN 是有意义的。

我注意到的另一件事是,使用不区分大小写的建模会更好,因为有些文本是大写,有些是小写,这种差异没有任何语义意义。因此我决定使用小写文本。

医学缩写更令人头疼。我考虑过扩展最常见的缩写(如比赛论坛中分享的那样),但这会导致复杂的 token 偏移映射。我决定只将最常见的情况相互映射。我的预处理文本函数是这样的:

def clean_abbrev(text):
    text = text.replace('FHx', 'FH ')
    text = text.replace('FHX', 'FH ')
    text = text.replace('PMHx', 'PMH ')
    text = text.replace('PMHX', 'PMH ')
    text = text.replace('SHx', 'SH ')
    text = text.replace('SHX', 'SH ')
    text = text.lower()
    return text

我做的另一项数据分析是检查标注边界落在 token 跨度内的位置。当这种情况发生时,基于 token 的模型无法正确学习如何正确预测这些边界。我对 roberta、deberta 和 debertav2 分词器做了这个检查。有两种情况涵盖了大多数情况:

  • 某些非空格文本前的空格
  • 某些文本前的 '\r\n'

因此,我创建了一个后处理函数,扩展了 @theoviel 的空格移除函数:

spaces = ' \n\r'

def post_process_spaces(pred, text):
    text = text[:len(pred)]
    pred = pred[:len(text)]
    if text[0] in spaces:
        pred[0] = 0
    if text[-1] in spaces:
        pred[-1] = 0

    for i in range(1, len(text) - 1):
        if text[i] in spaces:
            if pred[i] and not pred[i - 1]:  # space before
                pred[i] = 0

            if pred[i] and not pred[i + 1]:  # space after
                pred[i] = 0

            if pred[i - 1] and pred[i + 1]:
                pred[i] = 1
 
    return pred

还有第三种情况涵盖了大多数剩余情况:

  • 'yom' 和 'yof' 子字符串,分别代表岁女性或岁男性。跨度边界就在 'yo' 之后,但所有分词器都为 'of' 创建了一个 token,roberta 分词器为 'om' 创建了一个 token。在这两种情况下,边界都位于 'of' 或 'om' token 内部。

我根据特征对 token 'om' 和 'on' 进行了后处理。如果特征是关于性别的,我只保留第二个字符。如果特征是关于年龄的,我只保留第一个字符。这是我使用的代码。

def pred_to_chars(token_type_logits, len_token, max_token, offset_mapping, text, feature_num):
    token_type_logits = token_type_logits[:len_token]
    offset_mapping = offset_mapping[:len_token]
    char_preds = np.ones(len(text)) * -1e10
    for i, (start,end) in enumerate(offset_mapping):
        if text[start:end] == 'of' and start > 0 and text[start-1:end] == 'yof':
            if feature_num in feature_female:
                char_preds[end-1:end] = 1
            elif feature_num in feature_year:
                char_preds[start:start+1] = token_type_logits[i-1]
            else:
                char_preds[start:end] = token_type_logits[i]
        elif text[start:end] == 'om' and start > 0 and text[start-1:end] == 'yom':
            if feature_num in feature_male:
                char_preds[end-1:end] = 1
            elif feature_num in feature_year:
                char