Я пытаюсь создать собственную модель NER для идентификации объектов, связанных с кибербезопасностью (27 из них). Я решил использовать пустую модель, потому что думаю, что у меня достаточно большой (не уверен в этом) набор обучающих данных (~ 11 тысяч предложений, извлеченных из Википедии).
Для создания обучающих данных, необходимых для spaCy, я использовал утилиту PhraseMatcher. Идея состоит в том, чтобы сопоставить определенные предопределенные слова / фразы, относящиеся к объектам, которые я хочу идентифицировать, как показано ниже:
import spacy
from spacy.matcher import PhraseMatcher
nlp = spacy.load("en")
import pandas as pd
from tqdm import tqdm
from collections import defaultdict
Укажите метки сопоставления
users_pattern = [nlp(text) for text in ("user", "human", "person", "people", "end user")]
devices_pattern = [nlp(text) for text in ("device", "peripheral", "appliance", "component", "accesory", "equipment", "machine")]
accounts_pattern = [nlp(text) for text in ("account", "user account", "username", "user name", "loginname", "login name", "screenname", "screen name", "account name")]
identifiers_pattern = [nlp(text) for text in ("attribute", "id", "ID", "code", "ID code")]
authentication_pattern = [nlp(text) for text in ("authentication", "authenticity", "certification", "verification", "attestation", "authenticator", "authenticators")]
time_pattern = [nlp(text) for text in ("time", "date", "moment", "present", "pace", "moment")]
unauthorized_pattern = [nlp(text) for text in ("unauthorized", "illegal", "illegitimate", "pirated", "unapproved", "unjustified", "unofficial")]
disclosure_pattern = [nlp(text) for text in ("disclosure", "acknowledgment", "admission", "exposure", "advertisement", "divulgation")]
network_pattern = [nlp(text) for text in ("network", "net", "networking", "internet", "Internet")]
wireless_pattern = [nlp(text) for text in ("wireless", "wifi", "Wi-Fi", "wireless networking")]
password_pattern = [nlp(text) for text in ("password", "passwords", "passcode", "passphrase")]
configuration_pattern = [nlp(text) for text in ("configuration", "composition")]
signatures_pattern = [nlp(text) for text in ("signature", "signatures", "digital signature", "electronic signature")]
certificates_pattern = [nlp(text) for text in ("certificate", "digital certificates", "authorization certificate", "public key certificates", "PKI", "X509", "X.509")]
revocation_pattern = [nlp(text) for text in ("revocation", "annulment", "cancellation")]
keys_pattern = [nlp(text) for text in ("key", "keys")]
algorithms_pattern = [nlp(text) for text in ("algorithm", "algorithms", "formula", "program")]
standard_pattern = [nlp(text) for text in ("standard", "standards", "specification", "specifications", "norm", "rule", "rules", "RFC")]
invalid_pattern = [nlp(text) for text in ("invalid", "false", "unreasonable", "inoperative")]
access_pattern = [nlp(text) for text in ("access", "connection", "entry", "entrance")]
blocking_pattern = [nlp(text) for text in ("blocking", "block", "blacklist", "blocklist", "close", "cut off", "deter", "prevent", "stop")]
notification_pattern = [nlp(text) for text in ("notification", "notifications", "notice", "warning")]
messages_pattern = [nlp(text) for text in ("message", "messages", "note", "news")]
untrusted_pattern = [nlp(text) for text in ("untrusted", "malicious", "unsafe")]
security_pattern = [nlp(text) for text in ("security", "secure", "securely", "protect", "defend", "guard")]
symmetric_pattern = [nlp(text) for text in ("symmetric", "symmetric crypto")]
asymmetric_pattern = [nlp(text) for text in ("asymmetric", "asymmetric crypto")]
matcher = PhraseMatcher(nlp.vocab)
matcher.add("USER", None, *users_pattern)
matcher.add("DEVICE", None, *devices_pattern)
matcher.add("ACCOUNT", None, *accounts_pattern)
matcher.add("IDENTIFIER", None, *identifiers_pattern)
matcher.add("AUTHENTICATION", None, *authentication_pattern)
matcher.add("TIME", None, *time_pattern)
matcher.add("UNAUTHORIZED", None, *unauthorized_pattern)
matcher.add("DISCLOSURE", None, *disclosure_pattern)
matcher.add("NETWORK", None, *network_pattern)
matcher.add("WIRELESS", None, *wireless_pattern)
matcher.add("PASSWORD", None, *password_pattern)
matcher.add("CONFIGURATION", None, *configuration_pattern)
matcher.add("SIGNATURE", None, *signatures_pattern)
matcher.add("CERTIFICATE", None, *certificates_pattern)
matcher.add("REVOCATION", None, *revocation_pattern)
matcher.add("KEY", None, *keys_pattern)
matcher.add("ALGORITHM", None, *algorithms_pattern)
matcher.add("STANDARD", None, *standard_pattern)
matcher.add("INVALID", None, *invalid_pattern)
matcher.add("ACCESS", None, *access_pattern)
matcher.add("BLOCKING", None, *blocking_pattern)
matcher.add("NOTIFICATION", None, *notification_pattern)
matcher.add("MESSAGE", None, *messages_pattern)
matcher.add("UNTRUSTED", None, *untrusted_pattern)
matcher.add("SECURITY", None, *security_pattern)
matcher.add("SYMMETRIC", None, *symmetric_pattern)
matcher.add("ASYMMETRIC", None, *asymmetric_pattern)
Подготовить обучающие данные
def offsetter(lbl, doc, matchitem):
"""
Convert PhaseMatcher result to the format required in training (start, end, label)
"""
o_one = len(str(doc[0:matchitem[1]]))
subdoc = doc[matchitem[1]:matchitem[2]]
o_two = o_one + len(str(subdoc))
return (o_one, o_two, lbl)
to_train_ents = []
count_dic = defaultdict(int)
# Load the original sentences
df = pd.read_csv("sentences.csv", index_col=False)
phrases = df["sentence"].values
for line in tqdm(phrases):
nlp_line = nlp(line)
matches = matcher(nlp_line)
if matches:
for match in matches:
match_id = match[0]
start = match[1]
end = match[2]
label = nlp.vocab.strings[match_id] # get the unicode ID, i.e. 'COLOR'
span = nlp_line[start:end] # get the matched slice of the doc
count_dic[label] += 1
res = [offsetter(label, nlp_line, match)]
to_train_ents.append((line, dict(entities=res)))
count_dic = dict(count_dic)
TRAIN_DATA = to_train_ents
После выполнения вышеуказанного кода я получил обучающие данные в формате, требуемом spaCy. Эти предложения содержат интересующие меня объекты, которые распределены, как показано ниже:
print(sorted(count_dic.items(), key=lambda x:x[1], reverse=True), len(count_dic))
sum(count_dic.values())
[('NETWORK', 1962), ('TIME', 1489), ('USER', 1206), ('SECURITY', 981), ('DEVICE', 884), ('STANDARD', 796), ('ACCESS', 652), ('ALGORITHM', 651), ('MESSAGE', 605), ('KEY', 423), ('IDENTIFIER', 389), ('BLOCKING', 354), ('AUTHENTICATION', 141), ('WIRELESS', 109), ('UNAUTHORIZED', 99), ('CONFIGURATION', 89), ('ACCOUNT', 86), ('UNTRUSTED', 77), ('PASSWORD', 62), ('DISCLOSURE', 58), ('NOTIFICATION', 55), ('INVALID', 44), ('SIGNATURE', 41), ('SYMMETRIC', 23), ('ASYMMETRIC', 11), ('CERTIFICATE', 10), ('REVOCATION', 9)] 27
11306
Затем я использовал стандартную процедуру обучения для обучения пустой модели NER в spaCy, показанной ниже.
Обучение пустой модели
# define variables
model = None
n_iter = 100
if model is not None:
nlp_new = spacy.load(model) # load existing spaCy model
print("Loaded model '%s'" % model)
else:
nlp_new = spacy.blank("en") # create blank Language class
print("Created blank 'en' model")
# Add entity recognizer to model if it's not in the pipeline
# nlp.create_pipe works for built-ins that are registered with spaCy
if "ner" not in nlp_new.pipe_names:
ner = nlp_new.create_pipe("ner")
nlp_new.add_pipe(ner)
# otherwise, get it, so we can add labels to it
else:
ner = nlp_new.get_pipe("ner")
# add labels
for _, annotations in TRAIN_DATA:
for ent in annotations.get("entities"):
ner.add_label(ent[2])
# get names of other pipes to disable them during training
other_pipes = [pipe for pipe in nlp_new.pipe_names if pipe != "ner"]
with nlp_new.disable_pipes(*other_pipes): # only train NER
if model is None:
optimizer = nlp_new.begin_training()
else:
optimizer = nlp_new.resume_training()
# Set this based on this resource: spacy compounding batch size
sizes = compounding(1, 16, 1.001)
# batch up the examples using spaCy's minibatch
for itn in tqdm(range(n_iter)):
losses = {}
random.shuffle(TRAIN_DATA)
batches = minibatch(TRAIN_DATA, size=sizes)
for batch in batches:
texts, annotations = zip(*batch)
nlp_new.update(texts, annotations, sgd=optimizer, drop=0.2, losses=losses)
print("Losses", losses)
Окончательный проигрыш после этого - около 500.
Наконец, я проверил, как работает новая модель, используя данные обучения. Я ожидал бы восстановить столько сущностей, сколько было изначально указано в наборе обучающих данных. Однако после выполнения приведенного ниже кода я получаю только около 600 экземпляров из ~ 11k в общей сложности.
Модель, обученная тестированию
count_dic = defaultdict(int)
for text, _ in TRAIN_DATA:
doc = nlp_new(text)
for ent in doc.ents:
count_dic[ent.label_] += 1
print(sorted(count_dic.items(), key=lambda x:x[1], reverse=True), len(count_dic))
sum(count_dic.values())
[('TIME', 369), ('NETWORK', 47), ('IDENTIFIER', 41), ('BLOCKING', 28), ('USER', 22), ('STANDARD', 22), ('SECURITY', 15), ('MESSAGE', 15), ('ACCESS', 7), ('CONFIGURATION', 7), ('DEVICE', 7), ('KEY', 4), ('ALGORITHM', 3), ('SYMMETRIC', 2), ('UNAUTHORIZED', 2), ('SIGNATURE', 2), ('WIRELESS', 1), ('DISCLOSURE', 1), ('INVALID', 1), ('PASSWORD', 1), ('NOTIFICATION', 1)] 21
598
Интересно, почему с помощью этой процедуры получается модель с таким неподходящим поведением. Мне известны комментарии в этих сообщениях: Обучение NER с использованием Spacy и Пользовательский NER SPACY не возвращает никаких объектов, но они не решают мою проблему.
Надеюсь, вы сможете поделиться своим мнением о том, что я сделал и как я могу улучшить обнаружение сущностей в обучающем наборе. Я думал, что 11к предложений будет достаточно, если я не делаю что-то не так. Я использую Python 3.6.9 и spaCy 2.2.4.
Спасибо большое за вашу помощь.
Обновлять
Я решил обучить модель, включая как положительные, так и отрицательные образцы. Теперь обучающие данные содержат более 40 тысяч предложений. Однако это изменение не улучшает результат классификации в обучающем наборе. Есть другие предложения?
Набор обучающих данных
Полный набор данных обучения можно загрузить с здесь.