在自然语言处理的序列标注问题中,标签方案的使用可能因人而异,这里对常见的几种标签方案做简单的介绍。
标签说明
标签方案中通常都使用一些简短的英文字符[串]来编码。标签是打在token上的。对于英文,token可以是一个单词(e.g. awesome),也可以是一个字符(e.g. a)。对于中文,token可以是一个词语(分词后的结果),也可以是单个汉字字符。为便于说明,以下都将token试作等同于字符。
标签列表如下:
- B,即Begin,表示开始
- I,即Intermediate,表示中间
- E,即End,表示结尾
- S,即Single,表示单个字符
- O,即Other,表示其他,用于标记无关字符
常见标签方案
基于上面的标签列表,通过选择该列表的子集,可以得到不同的标签方案。同样的标签列表,不同的使用方法,也可以得到不同的标签方案。
常用的较为流行的标签方案有如下几种:
- IOB1: 标签I用于文本块中的字符,标签O用于文本块之外的字符,标签B用于在该文本块前面接续则一个同类型的文本块情况下的第一个字符。
- IOB2: 每个文本块都以标签B开始,除此之外,跟IOB1一样。
- IOE1: 标签I用于独立文本块中,标签E仅用于同类型文本块连续的情况,假如有两个同类型的文本块,那么标签E会被打在第一个文本块的最后一个字符。
- IOE2: 每个文本块都以标签E结尾,无论该文本块有多少个字符,除此之外,跟IOE1一样。
- START/END (也叫SBEIO、IOBES): 包含了全部的5种标签,文本块由单个字符组成的时候,使用S标签来表示,由一个以上的字符组成时,首字符总是使用B标签,尾字符总是使用E标签,中间的字符使用I标签。
- IO: 只使用I和O标签,显然,如果文本中有连续的同种类型实体的文本块,使用该标签方案不能够区分这种情况。
其中最常用的是IOB2、IOBS、IOBES。
举个栗子:
Example:| Bill| works| for| Bank| of| America| and| takes| the| Boston| Philadelphia| train. |:–:|:—–:|:–:|:–:|:–:|:–:|:–:|:–:|:–:|:–:|:–:|:–:|:–:| |IO: | I-PER | O | O | I-ORG | I-ORG | I-ORG | O | O | O | I-LOC | I-LOC | O| |IOB1: | I-PER | O | O | I-ORG | I-ORG | I-ORG | O | O | O | I-LOC B-LOC | O| |IOB2: | B-PER | O | O | B-ORG | I-ORG | I-ORG | O | O | O | B-LOC B-LOC | O| |IOE1: | I-PER | O | O | I-ORG | I-ORG | I-ORG | O | O | O | E-LOC I-LOC | O| |IOE2: | E-PER | O | O | I-ORG | I-ORG | E-ORG | O | O | O | E-LOC E-LOC | O| |BILOU: | U-PER | O | O | B-ORG | I-ORG | L-ORG | O | O | O | U-LOC U-LOC | O| |SBEIO: | S-PER | O | O | B-ORG | I-ORG | E-ORG | O | O | O | S-LOC S-LOC | O|
上面的BILOU方案猜想应该是跟SBEIO一致的,只不过使用的标签不完全一样,其中U(Unitary,单一的)应该对应S,L(Last,最后的)应该对应E。这里没有再做考证。
转换代码
iob1 -> iob2
def iob2(tags):
"""
Check that tags have a valid IOB format.
Tags in IOB1 format are converted to IOB2.
"""
for i, tag in enumerate(tags):
if tag == 'O':
continue
split = tag.split('-')
if len(split) != 2 or split[0] not in ['I', 'B']:
return False
if split[0] == 'B':
continue
elif i == 0 or tags[i - 1] == 'O': # conversion IOB1 to IOB2
tags[i] = 'B' + tag[1:]
elif tags[i - 1][1:] == tag[1:]:
continue
else: # conversion IOB1 to IOB2
tags[i] = 'B' + tag[1:]
return True
iob -> iobes
def iob_iobes(tags):
"""
IOB -> IOBES
"""
new_tags = []
for i, tag in enumerate(tags):
if tag == 'O':
new_tags.append(tag)
elif tag.split('-')[0] == 'B':
if i + 1 != len(tags) and \
tags[i + 1].split('-')[0] == 'I':
new_tags.append(tag)
else:
new_tags.append(tag.replace('B-', 'S-'))
elif tag.split('-')[0] == 'I':
if i + 1 < len(tags) and \
tags[i + 1].split('-')[0] == 'I':
new_tags.append(tag)
else:
new_tags.append(tag.replace('I-', 'E-'))
else:
raise Exception('Invalid IOB format!')
return new_tags
iobes -> iob
def iobes_iob(tags):
"""
IOBES -> IOB
"""
new_tags = []
for i, tag in enumerate(tags):
if tag.split('-')[0] == 'B':
new_tags.append(tag)
elif tag.split('-')[0] == 'I':
new_tags.append(tag)
elif tag.split('-')[0] == 'S':
new_tags.append(tag.replace('S-', 'B-'))
elif tag.split('-')[0] == 'E':
new_tags.append(tag.replace('E-', 'I-'))
elif tag.split('-')[0] == 'O':
new_tags.append(tag)
else:
raise Exception('Invalid format!')
return new_tags