V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
enenaaa
V2EX  ›  程序员

用条件随机场做网络小说命名实体识别

  •  1
     
  •   enenaaa · 2017-12-18 11:17:08 +08:00 · 5154 次点击
    这是一个创建于 2532 天前的主题,其中的信息可能已经有所发展或是发生改变。

    一直想用统计学习方法来改善拨云搜索,这次先在命名实体上小小尝试一下。

    线性链条件随机场

    对于无向图中的节点,定义一组特征函数,使其状态仅受邻近节点和观测序列的影响。

    在标注任务中,节点只有前后两个邻近节点,即线性链条件随机场。

    按照定义,节点有两类特征函数:转移特征函数和状态特征函数。转移特征函数表示上一个节点状态及观测序列对当前节点的影响,状态特征函数表示观测序列对当前节点的影响。每个函数都有个权重,模型经过训练后,通过函数及其权重即可推算节点状态。

    直观地看,对于 NER 任务,文本为观测序列,一个词对应一个节点,标签为节点状态。我们为每个节点定义一组特征函数,因为节点对应的词不一样,它们的特征函数也不一样。经过训练后,模型通过词对应的特征函数集合,即可计算出最可能的标签。

    python-CRFsuite

    因为要同 python 程序衔接,这里选用 CRFsuite 库。 与 CRF++晦涩的模板文件不同,CRFsuite 以键值对的形式定义特征函数。优点是灵活直观,缺点嘛就是不太好自定义转移特征了,不过对本文任务没影响。

    方案设计

    此次任务目标是识别小说里的地名、道具、技能、门派及特殊人名等实体。由于文本的特殊性,可以预见通用的训练语料效果不会太好。所以我用基于规则的方法来标注八本有代表性的小说,以此作为训练语料。用另外的四本小说来评测,然后与前述规则方法的结果做比较。

    训练语料的八本小说:凡人修仙传, 问题妹妹恋上我, 轮回剑典, 极品家丁, 雪鹰领主, 武林半侠传, 儒道之天下霸主, 无限之高端玩家。

    评测的四本小说:择天记,大道争锋,魔魂启临,俗人回档。

    标签采用 BMEO 四种状态,B 实体开始,M 实体中间词,E 实体结尾词,O 无关词。

    预期目标

    原有的规则识别是在匹配到特殊字词后,按照特定句式、字词及词频来判定是否命名实体。CRF 在定义特征时,综合考虑了上下文内容和词性,预计泛化会更好一点。但是语料标注及分词词性本身并不靠谱,估计准确率有所降低。

    实现

    1. 生成训练语料

    将文本分词后, 送入原识别程序。然后对结果标注,以 json 格式保存。格式如:

    ["这是", "r", "O"], ["清", "a", "B"], ["虚", "d", "M"], ["门", "n", "E"], ["的", "uj", "O"], ["飞行", "vn", "O"], ["法器", "n", "O"], ["雪", "n", "O"], ["虹", "n", "O"],
    

    单元里有文字、词性和标签 3 个属性。

    2. 定义特征

    以当前词及前后两个词的文字和词性为特征函数。这里都是状态特征,转移特征是默认的。

    def _word2features(wordlist, i):
        """ 返回特征列表 """
        features = [
            'bias',
            'word:'+wordlist[i][0],
            'word_attr:'+wordlist[i][1]
        ]
    
        if i > 0:
            features.append('word[-1]:'+wordlist[i-1][0])
            features.append('word[-1]_attr:'+wordlist[i-1][1])
            if i > 1:
                features.append('word[-2]:'+wordlist[i-2][0])
                features.append('word[-2,-1]:'+wordlist[i-2][0]+wordlist[i-1][0])
                features.append('word[-2]_attr:'+wordlist[i-2][1])
        if i < len(wordlist)-1:
            features.append('word[1]:'+wordlist[i+1][0])
            features.append('word[1]_attr:'+wordlist[i+1][1])
            if i < len(wordlist)-2:
                features.append('word[2]:'+wordlist[i+2][0])
                features.append('word[1,2]:'+wordlist[i+1][0]+wordlist[i+2][0])
                features.append('word[2]_attr:'+wordlist[i+2][1])
        return features
    

    这里直接把特征属性组合为名称, 这样其值就是函数权重,默认为 1。

    3. 训练模型

    默认用 L-BFGS 算法训练。 参数也并没有调到最优。

    model = pycrfsuite.Trainer(verbose=True)
    model.append(train_x, train_y)
    model.set_params({
        'c1': 1.0,   # coefficient for L1 penalty
        'c2': 1e-3,  # coefficient for L2 penalty
        'max_iterations': 100,  # stop earlier
        'feature.possible_states': True,
        # include transitions that are possible, but not observed
        'feature.possible_transitions': True,
        'feature.minfreq': 3
    })
    
    model.train("./test.crf")
    

    4. 评测

    tagger = pycrfsuite.Tagger()
    tagger.open('./test.crf')
    test_y = tagger.tag(test_x)
    

    这里抽选大道争锋的标注结果,其他 3 本也差不多,如下:

    '少清派', '邪派', '玄门正宗', '正宗玄门', '丹师', '魔宗', '莹云', '种法术', '蛇出来', '琴楠', '大玄门', '一玄门', '一身皮', '了莹', '大派', '丹术', '丹三', '紫眉', '丹主', '黑衣道人', '巧巧', '阵图', '喜欢道士', '
    十大玄门', '玄袍道人', '小派', '铜炉', '玄功', '剑招', '十多人', '二岛', '世家玄门', '九曲溪宫', ',玄门', '派玄门', '诸多玄门', '丹成功', '剑出来', '了玄门', '责翠', '源剑', '玄门小派', '魔门', '玄门十大派', '栖鹰', '三大玄门', '丹功', '丹境', '于各个玄门', '各大派', '一杆长枪', '丹出来', '阵门', '仙派', '九转功', '丹炉', '楚玄门', '青云一', '禽鹰', '凝功', '源经', '莹莹', '半声', '祭出来', '了出来', '黑风', '玄族', '可玄门', '师出来', '主宫', '玄门大派'
    

    规则方法结果:

    '此阵', '六大魔宗', '女冠', '化形丹', '蓬远派', '桂从尧', '功德院中', '参神契', '此水', '血魄', '自思', '玄门世家', '广源派', '一观', '双翅', '闯阵', '四象阵', '沉香舟', '澜云密册', '讨争', '补天阁', '道姑', '晁掌阁',
    '入山', '派中', '玄灵山', '清羽门下', '符书', '元阳派', '大门大派', '观容师妹', '入阵', '善渊观', '越真观', '幽阴重水', '水行真光', '玄光境界', '景管事', '破阵', '宝丰观中', '剑符', '修道者', '沉香教中', '管事', '万福一礼', '化
    丹修士', '山河童子', 'jī动', '年轻道人', '符诌', '紫眉道人', '斩神阵', '丹鼎院', '太昊派', '败下阵', '歉然', '此丹', '剑丸', '疑 huò', '此老', '溟沧派', '张衍神色', '甫一', '砀域水国', '血衣修士', '正 sè', '少清派', '两名老道', '
    魔门', '丹煞', '观中', '师徒一脉', '德修观', '候伯叙', '魔简', '入门弟子', '苍梧山', '老道', '小金丹', '宁冲玄一', '下院入门', '化丹', '小童', '符御卿', '力士', '众弟子', '中年修士', '文安', '文俊', '老魔', '熬通', '道诀', '丘老道', '手一', '玄光境', '丹鼎院中', '玄门正宗', '天阁', '血魄宗', '南华派', '年轻修士', '拿眼', '阵中', '小派', '北辰派', '太昊门', '化一', '阵门', '风师兄', '道书', '玲儿', '玄功', '琴楠', '儒雅道人', '神梭', '凝丹', '珍茗', '玄门十派', '沉香教', '张衍洒然', '无需多', '中年道人', '魔宗', '凕沧派', '清羽门', '守阵', '魔穴', '解读道书', '十六派', '九魁妖王', '秀儿', '执事道童', '了声', '化丹境界', '待张衍', '玄门大派', '陶真人门下', '两派'
    

    统计结果:

    | 项目 | CRF | 规则 |

    | ---- | ---- | ---- |

    | 实体数量 | 71 | 138 |

    | 都有 | 9 | 9 |

    | 差异数量 | 62 | 129 |

    | 准确数 | 25(35%) | 105(76%) |

    | 独有准确数 | 16(8) | -- |

    正确的标准是组合正确,且确实为新实体。有多种组合方案的,都算正确,例如:紫眉和紫眉道人。

    独有准确数括号里的数字去掉了因词频按规则方法应抛弃的实体。

    可以看出几点:

    • 规则方法识别的数量比 CRF 要多。这是因为语料偏少,CRF 只训练了 8 本小说,并没有学习到所有的规则信息。
    • 两者的准确率都有问题。例如 CRF 里的“了出来”,“师出来”。规则方式里的“甫一”,“正 sè”。都是错误地识别了人名,其原因各有不同,但 CRF 受到了标注语料的影响,有可能将错误率放大。
    • CRF 识别出了超出规则的实体,这是期望的目标。

    小结

    在不靠谱的训练语料及不靠谱的评价标准下,CRF 仍然给出了有意义的结果。

    15 条回复    2017-12-18 16:42:14 +08:00
    jy02201949
        1
    jy02201949  
       2017-12-18 11:59:00 +08:00   ❤️ 3
    我连标题都整不明白。。。
    neosfung
        2
    neosfung  
       2017-12-18 12:00:24 +08:00   ❤️ 1
    state of the art 的方法是 bi-lstm 后面接一个 crf 层
    现在几乎所有的领域都要用一下深度学习才显得自己潮流
    当然了,还是支持楼主的动手能力
    cosmic
        3
    cosmic  
       2017-12-18 12:13:52 +08:00   ❤️ 1
    https://github.com/zjy-ucas/ChineseNER 就是楼上说的 Bi-LSTM+CRF
    推荐一下用这个试试,目前我用这个做 NER 准确率,召回率都很高。当然,前提是你需要自己标注一些数据。
    Jface
        4
    Jface  
       2017-12-18 12:14:50 +08:00 via Android
    😂不明觉厉,文科生一脸懵逼
    enenaaa
        5
    enenaaa  
    OP
       2017-12-18 12:51:53 +08:00
    @neosfung
    @cosmic 谢谢。
    没理解错的话,bi-lstm 的作用是在观测序列中加入更多的上下文信息。 但是我理解里实体的影响距离应该是比较短的。如果加入长距离的上下文, 在跨领域的文本(假设不同风格的小说文本)中效果会不会变差
    winglight2016
        6
    winglight2016  
       2017-12-18 12:59:45 +08:00
    @cosmic 数据量很大的时候,怎么“自己标注”呢?我现在想分析几百兆的短信内容,抽取实体,大佬有没有高效方法推荐一下?
    cosmic
        7
    cosmic  
       2017-12-18 13:32:21 +08:00   ❤️ 1
    @enenaaa 基本上都是从句子层面出发来做 NER 的,距离影响的长短,会根据语料算出来的。对于不同风格的文章,我的建议是增加样本量
    cosmic
        8
    cosmic  
       2017-12-18 13:33:53 +08:00
    @winglight2016 可以去众包平台上找人标注。也可以先拿公开的语料训练,然后把错误的结果拿出来,人工修正加入语料里
    neosfung
        9
    neosfung  
       2017-12-18 14:19:18 +08:00   ❤️ 1
    @enenaaa

    lstm 的作用,就是捕捉长距离的信息,然后计算当前词语的各个 tag 的概率(point wise)。

    crf 放在最后一层的作用是,计算一个全局( sentence wise)的最优。等于 lstm 把每个词语的各个 tag 的概率算出来,crf 再在这里选择一个全局最优的标注序列(有点像 loss 函数,呵呵)。

    pytorch 的官方例子可以看这里 http://pytorch.org/tutorials/beginner/nlp/advanced_tutorial.html#bi-lstm-conditional-random-field-discussion

    _get_lstm_features 函数就是获得每个词语的 tag 的概率了
    winglight2016
        10
    winglight2016  
       2017-12-18 15:11:00 +08:00
    @cosmic 感谢大佬回复,不过这种方法似乎不适合个人操作,有没有无监督学习方法可以应用啊?
    natforum
        11
    natforum  
       2017-12-18 15:18:42 +08:00
    http://xuanpai.sinaapp.com/ 可以参考这个
    gouchaoer
        12
    gouchaoer  
       2017-12-18 15:21:03 +08:00
    卧槽,这个年代还有有人玩 crf
    gouchaoer
        13
    gouchaoer  
       2017-12-18 15:23:32 +08:00
    数学不好,当初没搞懂 crf
    scp055
        14
    scp055  
       2017-12-18 15:46:46 +08:00
    不知楼主这个项目有 github 吗,想学习一下。谢谢
    enenaaa
        15
    enenaaa  
    OP
       2017-12-18 16:42:14 +08:00
    @scp055 没。 不过主要代码都贴上面了。 剩下的就是分词、文件操作之类。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2679 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 15:18 · PVG 23:18 · LAX 07:18 · JFK 10:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.