Articles publiés précédemment Détecter les anomalies dans les phrases à l'aide de Universal Sentence Encoder Désormais, à l'aide de Universal Sentence Encoder (USE), la tâche de trouver le texte du rapport de titres mélangé avec le texte de Soseki Natsume est détectée comme une anomalie dans les données de direction. Je l'ai traité comme un problème. Cette fois, non seulement USE mais aussi ELMo et BERT sont utilisés pour résoudre le même type de tâches, 3 Comparons deux modèles d'encodeur.
ELMo et BERT, qui ont été pré-appris en japonais, utilisent le modèle publié par Stockmark.
Tous les calculs ont été effectués sur Google Colaboratory. BERT utilise la série TensorFlow 1.x et USE utilise la série TensorFlow 2.x, travaillez donc avec plusieurs ordinateurs portables comme indiqué ci-dessous pour diviser l'environnement.
data_preparation.ipynb
――Préparation des données
ʻELMo_BERT_embedding.ipynb ――Calcul du vecteur embarqué par ELMo et BERT ʻUSE_embedding.ipynb
――Calcul du vecteur incorporé par USE
ʻAnomaly_detection.ipynb` ―― Calcul de détection d'anomalie
Dans l'article précédent, j'ai principalement utilisé le texte du roman de Soseki Natsume et mélangé le texte du rapport sur les titres comme des données anormales, mais cette fois c'est précieux car le modèle de la valeur boursière a été pré-appris dans le corpus du domaine commercial. Le texte principal est le rapport sur les titres. Les données anormales seront collectées à partir du corpus d'actualités liveoor. Le corpus d'actualités de LIVEDOOR n'est pas une actualité ordinaire, mais un ensemble de données composé d'articles tels que les appareils électroménagers, le sport et les potins de divertissement.
Tout d'abord, importez les bibliothèques requises et montez le lecteur Google.
data_preparation.ipynb
import re
import json
import glob
import numpy as np
from sklearn.model_selection import train_test_split
from google.colab import drive
drive.mount('/content/drive')
Dans ce qui suit, il est supposé que vous travaillez dans un répertoire appelé anonymous_detection sous My Drive. Veuillez remplacer le nom du répertoire comme il convient.
data_preparation.ipynb
%cd /content/drive/'My Drive'/anomaly_detection
Tout d'abord, téléchargez chABSA Dataset et extrayez uniquement le texte du rapport titres. La procédure est la même que dans l'article précédent.
data_preparation.ipynb
#Télécharger et extraire des données
!wget https://s3-ap-northeast-1.amazonaws.com/dev.tech-sketch.jp/chakki/public/chABSA-dataset.zip
!unzip chABSA-dataset.zip
!rm chABSA-dataset.zip
#Créer une liste de chemins d'accès aux fichiers
chabsa_path_list = glob.glob("chABSA-dataset/*.json")
#Stocker uniquement la partie texte du rapport sur les titres dans la liste
chabsa_texts = []
for p in chabsa_path_list:
with open(p, "br") as f:
j = json.load(f)
for line in j["sentences"]:
chabsa_texts += [line["sentence"].replace('\n', '')]
print(len(chabsa_texts))
# 6119
Supprimez les phrases trop courtes et les phrases trop longues.
data_preparation.ipynb
def filter_by_length(texts_input, min_len=20, max_len=300):
texts_output = []
for t in texts_input:
length = len(t)
if length >= min_len and length <= max_len:
texts_output.append(t)
return texts_output
chabsa_texts = filter_by_length(chabsa_texts)
print(len(chabsa_texts))
# 5148
Téléchargez ensuite le corpus d'actualités de Liveoor J'utiliserai sports-watch sur plusieurs articles.
data_preparation.ipynb
#Télécharger et extraire des données
!wget https://www.rondhuit.com/download/ldcc-20140209.tar.gz
!tar -xf ldcc-20140209.tar.gz
!rm ldcc-20140209.tar.gz
#Créer une liste de chemins d'accès aux fichiers
livedoor_path_list = glob.glob('./text/sports-watch/sports-watch-*.txt')
len(livedoor_path_list)
# 900
Étant donné que le texte du corpus d'actualités de livingoor contient de nombreux symboles tels que ■, le prétraitement est effectué à l'exclusion des symboles répertoriés. De plus, j'exclure toute la phrase, y compris l'URL. En outre, excluez les lignes composées uniquement de caractères alphanumériques, telles que les lignes liées aux droits d'auteur.
data_preparation.ipynb
def cleaning_text(texts):
#Exclut les instructions contenant des URL
p = re.compile('https?://')
if p.search(texts):
return ''
#Exclut les lignes contenant uniquement des caractères alphanumériques
p = re.compile('[\w /:%#\$&\?\(\)~\.,=\+\-…()<>]+')
if p.fullmatch(texts):
return ''
#Exclut les sauts de ligne, les espaces pleine largeur et les symboles fréquents
remove_list = ['\n', '\u3000', ' ', '<', '>', '・',
'■', '□', '●', '○', '◆', '◇',
'★', '☆', '▲', '△', '※', '*', '*', '——']
for s in remove_list:
texts = texts.replace(s, '')
return texts
livedoor_texts = []
for path in livedoor_path_list:
with open(path) as f:
texts = f.readlines()
livedoor_texts += [cleaning_text(t) for t in texts[4:]] #Pas besoin des 3 premières lignes
print(len(livedoor_texts))
Divisez la phrase par ponctuation pour qu'elle corresponde à la phrase et au format des données chABSA, et filtrez par la longueur de la phrase.
data_preparation.ipynb
def split_texts(texts_input, split_by='。'):
texts_output = []
for t in texts_input:
texts_output += t.split(split_by)
return texts_output
livedoor_texts = split_texts(livedoor_texts)
print(len(livedoor_texts))
# 17522
livedoor_texts = filter_by_length(livedoor_texts)
print(len(livedoor_texts))
# 8149
Maintenant que nous avons une liste de deux types de phrases, nous allons créer des données de développement de modèle et des données de test. L'étiquette 0 est attribuée au texte du rapport sur les titres et l'étiquette 1 est attribuée au texte des news de livingoor. 80% du texte du rapport titres seront utilisés pour les données de développement et les 20% restants seront utilisés pour les données de test. Seulement 1% environ du texte d'actualité de Livingoor est mélangé dans les données de développement. La quantité des deux types de phrases dans les données de test est de 50% chacun afin que la précision puisse être facilement évaluée.
data_preparation.ipynb
def make_dataset(main_texts, anom_texts, main_dev_rate=0.8, anom_dev_rate=0.01):
len1 = len(main_texts)
len2 = len(anom_texts)
num_dev1 = int(len1 * main_dev_rate)
num_dev2 = int(num_dev1 * anom_dev_rate)
num_test1 = len1 - num_dev1
num_test2 = num_test1
print("Données de développement principal: {}, anom: {}".format(num_dev1, num_dev2))
print("Données de test principal: {}, anom: {}".format(num_test1, num_test2))
main_arr = np.hstack([np.reshape(np.zeros(len1), (-1, 1)),
np.reshape(main_texts, (-1, 1))])
anom_arr = np.hstack([np.reshape(np.ones(len2), (-1, 1)),
np.reshape(anom_texts, (-1, 1))])
dev1, test1 = train_test_split(main_arr, train_size=num_dev1)
dev2, test2 = train_test_split(anom_arr, train_size=num_dev2)
test2, _ = train_test_split(test2, train_size=num_test2)
dev_arr = np.vstack([dev1, dev2])
np.random.shuffle(dev_arr)
test_arr = np.vstack([test1, test2])
np.random.shuffle(test_arr)
return dev_arr, test_arr
dev_arr, test_arr = make_dataset(chabsa_texts, livedoor_texts)
#Données de développement principal: 4118, anom: 41
#Données de test principal: 1030, anom: 1030
print(dev_arr.shape, test_arr.shape)
# (4159, 2) (2060, 2)
Enregistrez l'ensemble de données que vous avez créé.
data_preparation.ipynb
np.save('dev.npy', dev_arr)
np.save('test.npy', test_arr)
Voici un échantillon de certaines données de test.
1: Le jeu négligent de Rakuten aurait certainement été une grande flamme s'il avait été vaincu par Seibu tel quel 0: Le solde de gestion du fonds était de 17,9 milliards de yens (en hausse de 8,4% d'une année sur l'autre) en raison d'une augmentation des intérêts sur les prêts, bien que les intérêts et les dividendes sur les titres aient diminué. 1: Je le connais depuis qu'il a 18 ans, mais maintenant il a vraiment grandi 0: Avec la mise en place du navigateur de succession d'entreprise, nous réaliserons des activités de vente pour répondre aux besoins des managers de taille moyenne (ETI) qui souhaitent sélectionner le meilleur plan de succession d'entreprise parmi plusieurs options sur une durée relativement longue. Je vais 0: En ce qui concerne le bénéfice sectoriel, 3 977 en raison d'une augmentation des coûts liés à la qualité due aux gonfleurs d'airbag et diverses dépenses centrées sur les coûts de vente en raison de la hausse des taux d'intérêt aux États-Unis, des effets des fluctuations de change et d'une augmentation des coûts de test et de recherche Le bénéfice a diminué de 146 milliards de yens (26,8%) par rapport à l'exercice précédent consolidé à 100 millions de yens. De plus, depuis que nous sommes venus, je pense qu'il y a eu une fois où j'ai voulu que le réalisateur l'approuve. Et la 1ère place était "Je me demande si c'était le seul qui a été botté exactement comme je l'imaginais. 0: Bien que la manutention du fret aérien importé soit restée ferme, les ventes ont été de 101,7 milliards de yens, soit une baisse de 13,3 milliards de yens ou 11,6% par rapport à l'exercice précédent consolidé, et le bénéfice d'exploitation était de 1,1 milliard de yens en raison des effets des taux de change et d'autres facteurs. Par rapport à l'exercice précédent consolidé, le bénéfice a diminué de 500 millions de yens, soit 33,5%. 0: Dans l'économie japonaise, alors que l'environnement de l'emploi et des revenus a continué de s'améliorer, l'économie a continué d'afficher une tendance à la reprise progressive, même si certains retards dans l'amélioration ont été constatés. 0: En ce qui concerne l'activité de distribution de contenu PC, nous exploiterons des sites de fan-clubs payants pour les PC tels que les artistes et les talents, et réaliserons la production sous contrat de sites officiels, ce qui conduira à de nouveaux bénéfices à l'avenir, y compris d'autres divisions commerciales. Nous avons développé notre activité dans cet esprit.
De là, vous travaillerez sur le notebook ELMo_BERT_embedding.ipynb. Tout d'abord, montez Google Drive de la même manière qu'avant.
ELMo_BERT_embedding.ipynb
from google.colab import drive
drive.mount('/content/drive')
Le modèle de pré-apprentissage de la bourse utilise le dictionnaire MeCab + NEologd comme tokenizer. Suivez la page de distribution officielle pour installer le dictionnaire MeCab + NEologd. Tout d'abord, exécutez la commande suivante sur l'ordinateur portable pour installer MeCab lui-même.
ELMo_BERT_embedding.ipynb
!apt install aptitude swig
!aptitude install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file -y
Ensuite, installez le dictionnaire NEologd. Si vous travaillez sous le répertoire "Mon lecteur", vous obtiendrez une erreur car le nom du répertoire contient des espaces, donc travaillez dans le répertoire racine.
ELMo_BERT_embedding.ipynb
%cd /content
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
%cd mecab-ipadic-neologd
!echo yes | ./bin/install-mecab-ipadic-neologd -n
%cd /content/drive/'My Drive'/anomaly_detection2
Enfin, installez la bibliothèque pour appeler MeCab avec python.
ELMo_BERT_embedding.ipynb
!pip install mecab-python3
import MeCab
Dans la version japonaise d'ELMo utilisée cette fois, il est nécessaire de saisir le token divisé par MeCab, alors lisez le jeu de données créé précédemment et convertissez-le en token.
ELMo_BERT_embedding.ipynb
%cd /content/drive/'My Drive'/anomaly_detection
dev_arr = np.load('dev.npy')
test_arr = np.load('test.npy')
#Ne découpez que des phrases
dev_texts = dev_arr[:, 1]
test_texts = test_arr[:, 1]
def MeCab_tokenizer(texts):
mecab = MeCab.Tagger(
"-Owakati -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd")
token_list = []
for s in texts:
parsed = mecab.parse(s).replace('\n', '')
token_list += [parsed.split()]
return token_list
dev_tokens = MeCab_tokenizer(dev_texts)
test_tokens = MeCab_tokenizer(test_texts)
De plus, comme le programme BERT utilisé cette fois-ci doit être entré à partir d'un fichier texte, écrivez-le dans le fichier. Cela n'a pas besoin d'être symbolisé à l'avance.
ELMo_BERT_embedding.ipynb
with open('dev_text.txt', mode='w') as f:
f.write('\n'.join(dev_texts))
with open('test_text.txt', mode='w') as f:
f.write('\n'.join(test_texts))
ELMo Le modèle ELMo utilise cette implémentation (https://github.com/HIT-SCIR/ELMoForManyLangs) avec des paramètres pré-entraînés en bourse. L'utilisation spécifique est
Je l'ai mentionné. Tout d'abord, installez les bibliothèques requises et clonez le référentiel.
ELMo_BERT_embedding.ipynb
!pip install overrides
!git clone https://github.com/HIT-SCIR/ELMoForManyLangs.git
Exécutez setup.py
pour terminer l'installation.
ELMo_BERT_embedding.ipynb
%cd ./ELMoForManyLangs
!python setup.py install
%cd ..
Ensuite, téléchargez le modèle pré-entraîné Stockmark à partir de ce lien. Il existe deux types de modèles: «modèle intégré basé sur des mots» et «modèle intégré basé sur des caractères / basé sur des mots». Cette fois, nous utiliserons le "modèle embarqué basé sur des mots". J'ai placé le fichier téléchargé dans le dossier . / ELMo_ja_word_level
du Google Drive monté. Le contenu du dossier doit ressembler à ceci:
ELMo_BERT_embedding.ipynb
!ls ./ELMo_ja_word_level/
# char.dic config.json configs encoder.pkl token_embedder.pkl word.dic
Le modèle est maintenant prêt. ʻCréer une instance Embedder` et définir une fonction pour calculer le vecteur incorporé du texte.
ELMo_BERT_embedding.ipynb
from ELMoForManyLangs.elmoformanylangs import Embedder
from overrides import overrides
elmo_model_path = "./ELMo_ja_word_level"
elmo_embedder = Embedder(elmo_model_path, batch_size=64)
def get_elmo_embeddings(token_list, batch_size=64):
length = len(token_list)
n_loop = int(length / batch_size) + 1
sent_emb = []
for i in range(n_loop):
token_emb = elmo_embedder.sents2elmo(
token_list[batch_size*i: min(batch_size*(i+1), length)])
for emb in token_emb:
# sum over tokens to obtain the embedding for a sentence
sent_emb.append(sum(emb))
return np.array(sent_emb)
La sortie du modèle est un vecteur intégré pour chaque jeton d'entrée. La somme de tous les vecteurs de jeton dans le texte est utilisée comme vecteur d'incorporation de texte. La dimension du vecteur ELMo est 1024.
ELMo_BERT_embedding.ipynb
dev_elmo_embeddings = get_elmo_embeddings(dev_tokens)
test_elmo_embeddings = get_elmo_embeddings(test_tokens)
print(dev_elmo_embeddings.shape, test_elmo_embeddings.shape)
# (4159, 1024) (2060, 1024)
np.save('dev_elmo_embeddings.npy', dev_elmo_embeddings)
np.save('test_elmo_embeddings.npy', test_elmo_embeddings)
BERT
Pour BERT, nous utiliserons le modèle pré-entraîné publié par Stockmark. J'ai téléchargé la version de TensorFlow à partir de ce lien de téléchargement et l'ai placée dans le dossier . / BERT_base_stockmark
. Le contenu du dossier est le suivant.
ELMo_BERT_embedding.ipynb
!ls ./BERT_base_stockmark
# bert_config.json vocab.txt output_model.ckpt.index output_model.ckpt.meta output_model.ckpt.data-00000-of-00001
Importez tensorflow en spécifiant la série version 1.x.
ELMo_BERT_embedding.ipynb
%tensorflow_version 1.x
import tensorflow as tf
Clonez le corps du modèle à partir du référentiel officiel (https://github.com/google-research/bert).
ELMo_BERT_embedding.ipynb
!git clone https://github.com/google-research/bert.git
Le référentiel officiel a un code de script ʻextract_features.py` pour extraire les vecteurs incorporés, donc s'il s'agit de la version originale anglaise, vous pouvez simplement l'exécuter, mais pour utiliser le modèle de pré-apprentissage japonais Nécessite de modifier certains fichiers.
Suivez les instructions d'utilisation de cette page et modifiez tokenization.py
pour utiliser MeCab comme tokenizer. Tout d'abord, changez la fonction convert_tokens_to_ids (vocab, tokens)
pour qu'elle renvoie 1 qui est l'id de [UNK] pour les mots inconnus comme suit.
bert/tokenization.py
def convert_tokens_to_ids(vocab, tokens):
# modify so that it returns id=1 which means [UNK] when a token is not in vocab
output = []
for t in tokens:
if t in vocab.keys():
i = vocab[t]
else: # if t is [UNK]
i = 1
output.append(i)
return output
De plus, réécrivez la classe FullTokenizer (object)
comme suit pour utiliser MecabTokenizer au lieu de WordpieceTokenizer. D'autres BERT japonais pré-appris ont MeCab ou Human ++ qui les divise en éléments morphologiques puis appliquent Wordpiece Tokenizer, mais le modèle boursier utilise uniquement Mecab.
bert/tokenization.py
class FullTokenizer(object):
"""Runs end-to-end tokenziation."""
def __init__(self, vocab_file, do_lower_case=True):
self.vocab = load_vocab(vocab_file)
self.inv_vocab = {v: k for k, v in self.vocab.items()}
#self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
#self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab)
# use Mecab
self.mecab_tokenizer = MecabTokenizer()
def tokenize(self, text):
split_tokens = []
# for token in self.basic_tokenizer.tokenize(text):
# use Mecab
for token in self.mecab_tokenizer.tokenize(text):
split_tokens.append(token)
return split_tokens
def convert_tokens_to_ids(self, tokens):
#return convert_by_vocab(self.vocab, tokens)
return convert_tokens_to_ids(self.vocab, tokens)
def convert_ids_to_tokens(self, ids):
return convert_by_vocab(self.inv_vocab, ids)
Enfin, ajoutez la classe MecabTokenizer
qui hérite de BasicTokenizer
.
bert/tokenization.py
class MecabTokenizer(BasicTokenizer):
def __init__(self):
import MeCab
path = "-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd"
self._mecab = MeCab.Tagger(path)
def tokenize(self, text):
"""Tokenizes a piece of text."""
text = convert_to_unicode(text.replace(' ', ''))
text = self._clean_text(text)
mecab_result = self._mecab.parseToNode(text)
split_tokens = []
while mecab_result:
if mecab_result.feature.split(",")[0] != 'BOS/EOS':
split_tokens.append(mecab_result.surface)
mecab_result = mecab_result.next
output_tokens = whitespace_tokenize(" ".join(split_tokens))
print(split_tokens)
return output_tokens
Vous êtes maintenant prêt à utiliser BERT qui a été pré-appris en japonais, mais ʻextract_features.py génère un fichier qui stocke les vecteurs incorporés pour tous les jetons de la phrase entrée, donc il sera affiché s'il y a beaucoup de phrases. La taille du fichier sera grande. Dans cette tâche, nous n'utiliserons que le vecteur incorporé du texte, pas le vecteur incorporé de chaque jeton, donc nous réécrirons ʻextract_features.py
pour ne produire que cela. En tant que vecteur d'intégration de la phrase, la moyenne des vecteurs de tous les jetons est utilisée comme dans ELMo. En outre, pour le calque qui extrait le vecteur incorporé, le code d'origine génère le vecteur du calque spécifié, mais changez-le de sorte que les vecteurs de toutes les couches spécifiées soient moyennés. La sortie est enregistrée au format npy de numpy, donc dans la partie en-tête
bert/extract_features.py
import numpy as np
Est ajouté. De plus, modifiez la dernière partie de la fonction principale, ʻinput_fn = input_fn_builder (après la ligne`, comme suit.
bert/extract_features.py
input_fn = input_fn_builder(
features=features, seq_length=FLAGS.max_seq_length)
arr = []
for result in estimator.predict(input_fn, yield_single_examples=True):
cnt = 0
for (i, token) in enumerate(feature.tokens):
for (j, layer_index) in enumerate(layer_indexes):
layer_output = result["layer_output_%d" % j]
if token != '[CLS]' and token != '[SEP]':
if cnt == 0:
averaged_emb = np.array(layer_output[i:(i + 1)].flat)
else:
averaged_emb += np.array(layer_output[i:(i + 1)].flat)
cnt += 1
averaged_emb /= cnt
arr += [averaged_emb]
np.save(FLAGS.output_file, arr)
Maintenant que vous êtes prêt, exécutez la commande suivante dans votre notebook ELMo_BERT.ipynb. En tant que couche d'extraction du vecteur incorporé, les 5 dernières couches excluant la couche finale seront utilisées.
ELMo_BERT_embedding.ipynb
#BERT exécution dev
!python ./bert/extract_features.py \
--input_file=dev_text.txt \
--output_file=dev_bert_embeddings.npy \
--vocab_file=./BERT_base_stockmark/vocab.txt \
--bert_config_file=./BERT_base_stockmark/bert_config.json \
--init_checkpoint=./BERT_base_stockmark/output_model.ckpt \
--layers -2,-3,-4,-5,-6
ELMo_BERT_embedding.ipynb
#Test d'exécution BERT
!python ./bert/extract_features.py \
--input_file=test_text.txt \
--output_file=test_bert_embeddings.npy \
--vocab_file=./BERT_base_stockmark/vocab.txt \
--bert_config_file=./BERT_base_stockmark/bert_config.json \
--init_checkpoint=./BERT_base_stockmark/output_model.ckpt \
--layers -2,-3,-4,-5,-6
USE USE utilise une version allégée (Multilingual, CNN version, v3) des modèles appris en plusieurs langues, y compris le japonais. .. L'utilisation est celle expliquée dans Article précédent. TensorFlow utilise la série version 2.x.
USE_embedding.ipynb
import tensorflow_hub as hub
import tensorflow_text
import tensorflow as tf
from google.colab import drive
drive.mount('/content/drive')
USE_embedding.ipynb
%cd /content/drive/'My Drive'/anomaly_detection
use_url = 'https://tfhub.dev/google/universal-sentence-encoder-multilingual/3'
embed = hub.load(use_url)
def get_use_embeddings(texts, batch_size=100):
length = len(texts)
n_loop = int(length / batch_size) + 1
embeddings = use_embedder(texts[: batch_size])
for i in range(1, n_loop):
arr = use_embedder(texts[batch_size*i: min(batch_size*(i+1), length)])
embeddings = tf.concat([embeddings, arr], axis=0)
return np.array(embeddings)
USE_embedding.ipynb
dev_use_embeddings = get_use_embeddings(dev_arr[:, 1])
test_use_embeddings = get_use_embeddings(test_arr[:, 1])
print(dev_use_embeddings.shape, test_use_embeddings.shape)
# (4159, 512) (2060, 512)
np.save('dev_use_embeddings.npy', dev_use_embeddings)
np.save('test_use_embeddings.npy', test_use_embeddings)
Maintenant que nous avons les vecteurs intégrés pour les trois modèles, nous allons construire un modèle de détection d'anomalies et comparer la précision. Chargez les données de développement / test et les données vectorielles intégrées sur le notebook anomaly_detection.ipynb.
anomaly_detection.ipynb
import numpy as np
from scipy.stats import chi2
from google.colab import drive
drive.mount('/content/drive')
anomaly_detection.ipynb
%cd /content/drive/'My Drive'/anomaly_detection
dev_arr = np.load('dev.npy')
test_arr = np.load('test.npy')
dev_elmo_embeddings = np.load('dev_elmo_embeddings.npy')
test_elmo_embeddings = np.load('test_elmo_embeddings.npy')
dev_bert_embeddings = np.load('dev_bert_embeddings.npy')
test_bert_embeddings = np.load('test_bert_embeddings.npy')
dev_use_embeddings = np.load('dev_use_embeddings.npy')
test_use_embeddings = np.load('test_use_embeddings.npy')
print(dev_elmo_embeddings.shape, test_elmo_embeddings.shape)
# (4159, 1024) (2060, 1024)
print(dev_bert_embeddings.shape, test_bert_embeddings.shape)
# (4159, 768) (2060, 768)
print(dev_use_embeddings.shape, test_use_embeddings.shape)
# (4159, 512) (2060, 512)
Pour plus d'informations sur le modèle de détection des anomalies, consultez l'article précédent (https://qiita.com/jovyan/items/e5d2dc7ffabc2353db38). Le code du modèle est résumé dans la classe suivante DirectionalAnomalyDetection
.
anomaly_detection.ipynb
class DirectionalAnomalyDetection:
def __init__(self, dev_embeddings, test_embeddings, dev_arr):
self.dev_embeddings = self.normalize_arr(dev_embeddings)
self.test_embeddings = self.normalize_arr(test_embeddings)
self.dev_arr = dev_arr
self.mu, self.anom = self.calc_anomality(self.dev_embeddings)
self.mhat, self.shat = self.calc_chi2params(self.anom)
print("mhat: {:.3f}".format(self.mhat))
print("shat: {:.3e}".format(self.shat))
self.anom_test = None
def normalize_arr(self, arr):
norm = np.apply_along_axis(lambda x: np.linalg.norm(x), 1, arr) # norm of each vector
normed_arr = arr / np.reshape(norm, (-1,1))
return normed_arr
def calc_anomality(self, embeddings):
mu = np.mean(embeddings, axis=0)
mu /= np.linalg.norm(mu)
anom = 1 - np.inner(mu, embeddings)
return mu, anom
def calc_chi2params(self, anom):
anom_mean = np.mean(anom)
anom_mse = np.mean(anom**2) - anom_mean**2
mhat = 2 * anom_mean**2 / anom_mse
shat = 0.5 * anom_mse / anom_mean
return mhat, shat
def decide_ath_by_alpha(self, alpha, x_ini, max_ite=100, eps=1.e-12):
# Newton's method
x = x_ini
for i in range(max_ite):
xnew = x - (chi2.cdf(x, self.mhat, loc=0, scale=self.shat)
- (1 - alpha)) / chi2.pdf(x, self.mhat, loc=0, scale=self.shat)
if abs(xnew - x) < eps:
print("iteration: ", i+1)
break
x = xnew
print("ath: {:.4f}".format(x))
return x
def decide_ath_by_labels(self, x_ini, max_ite=100, eps=1.e-12):
anom0 = self.anom[self.dev_arr[:, 0] == '0.0']
anom1 = self.anom[self.dev_arr[:, 0] == '1.0']
mhat0, shat0 = self.calc_chi2params(anom0)
mhat1, shat1 = self.calc_chi2params(anom1)
ath = self._find_crossing_point(mhat0, shat0, mhat1, shat1, x_ini, max_ite, eps)
print("ath: {:.4f}".format(ath))
return ath
def _find_crossing_point(self, mhat0, shat0, mhat1, shat1, x_ini, max_ite, eps):
# Newton's method
x = x_ini
for i in range(max_ite):
xnew = x - self._crossing_func(x, mhat0, shat0, mhat1, shat1)
if abs(xnew - x) < eps:
print("iteration: ", i+1)
break
x = xnew
return x
def _crossing_func(self, x, mhat0, shat0, mhat1, shat1):
chi2_0 = chi2.pdf(x, mhat0, loc=0, scale=shat0)
chi2_1 = chi2.pdf(x, mhat1, loc=0, scale=shat1)
nume = x * (chi2_0 - chi2_1)
deno = (mhat0 * 0.5 - 1 - x / shat0 * 0.5) * chi2_0 \
-(mhat1 * 0.5 - 1 - x / shat1 * 0.5) * chi2_1
return nume / deno
def predict(self, ath):
self.anom_test = 1 - np.inner(self.mu, self.test_embeddings)
predict_arr = (self.anom_test > ath).astype(np.int)
return predict_arr
Créez une instance de modèle pour chacun des trois types de données. Les paramètres $ \ hat {m} $ et $ \ hat {s} $ lorsque la distribution des anomalies est ajustée avec la distribution du chi carré sont affichés.
anomaly_detection.ipynb
#-- ELMo
dad_elmo = DirectionalAnomalyDetection(dev_elmo_embeddings, test_elmo_embeddings, dev_arr)
# mhat: 6.813
# shat: 3.011e-03
#-- BERT
dad_bert = DirectionalAnomalyDetection(dev_bert_embeddings, test_bert_embeddings, dev_arr)
# mhat: 20.358
# shat: 6.912e-03
#-- USE
dad_use = DirectionalAnomalyDetection(dev_use_embeddings, test_use_embeddings, dev_arr)
# mhat: 36.410
# shat: 1.616e-02
La dimension effective $ \ hat {m} $ pour tout modèle est beaucoup plus petite que la dimension vectorielle réelle incorporée. Il est intéressant de noter que plus la dimension du vecteur intégré réel est grande (ELMo: 1024, BERT: 768, USE: 512), plus la dimension effective est petite. La distribution du chi carré avec les paramètres ainsi déterminés est tracée avec la distribution réelle du degré d'anomalie comme suit. Reflétant la petitesse de la dimension d'anomalie, la valeur de crête de la distribution d'anomalie est également petite dans BERT et en particulier ELMo. On peut également voir que l'ajustement de la distribution du chi carré est relativement médiocre pour ELMo et BERT.
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/fadbad99-0e34-1c35-55c2-699617b0f370.png ", height=200><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/e4c3dee9-2af5-d68a-966c-59ebebf7c52a.png ", height=200><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/909631e1-a7f4-8eb0-14f1-f3d2414b7b1f.png ", height=200>
Ensuite, trouvez le seuil $ a_ \ text {th} $ pour le degré d'anomalie. Dans l'article précédent, la formule du taux de faux positifs prédéterminé $ \ alpha $
decide_ath_by_alpha
de la classe définie ci-dessus. Seuls les résultats sont présentés, mais cette méthode entraîne une précision de classification nettement inférieure pour les données d'essai dans les trois modèles. La valeur du taux de fausses alarmes est de 0,01, ce qui correspond à la fois précédente.
Accuracy | Precision | Recall | F1 | |
---|---|---|---|---|
ELMo | 0.568 | 0.973 | 0.141 | 0.246 |
BERT | 0.580 | 0.946 | 0.170 | 0.288 |
USE | 0.577 | 0.994 | 0.155 | 0.269 |
Pour voir pourquoi il est si inexact, tracez l'histogramme des valeurs aberrantes pour les données de test séparément pour les données normales et anormales. La montagne sur la gauche est des données normales, et la montagne sur la droite est des données anormales. La ligne verticale rouge représente le seuil $ a_ \ text {th} $ obtenu par decide_ath_by_alpha
. Seuls les résultats pour USE sont affichés ici, mais les résultats pour les autres modèles sont similaires. Bien que les données normales et les données anormales puissent être séparées dans une certaine mesure, on peut voir que la plupart des données sont classées comme données normales parce que le seuil est trop grand.
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/800c812d-897d-8472-bac2-0f6bf5431e97.png ", height=240>
Dans les résultats présentés dans l'article précédent, la distribution du chi carré correspond bien à la distribution du degré d'anomalie, et la distribution des données normales et la distribution des données anormales sont assez clairement séparées, de sorte qu'une précision élevée est obtenue avec le seuil déterminé par la méthode ci-dessus. A été obtenu. Cependant, cela n'a pas fonctionné pour ces données car le chevauchement entre les données normales et la distribution anormale des données est relativement important.
Maintenant, je voudrais déterminer le seuil $ a_ \ text {th} $ par une autre méthode, mais il ne semble pas y avoir d'autre moyen dans le cadre de l'apprentissage non supervisé. Par conséquent, nous définirons le seuil dans le cadre d'apprentissage supervisé pour lequel l'indicateur normal / anormal des données de développement est connu. Plus précisément, suivez les étapes suivantes.
decide_ath_by_labels
de la classe DirectionalAnomalyDetection
, donc exécutez-la comme suit.anomaly_detection.ipynb
#-- ELMo
ath_elmo = dad_elmo.decide_ath_by_labels(x_ini=0.02)
# iteration: 5
# ath: 0.0312
#-- BERT
ath_bert = dad_bert.decide_ath_by_labels(x_ini=0.2)
# iteration: 6
# ath: 0.1740
#-- USE
ath_use = dad_use.decide_ath_by_labels(x_ini=0.8)
# iteration: 5
# ath: 0.7924
Le seuil calculé est utilisé pour faire des prédictions pour les données de test et évaluer la précision.
anomaly_detection.ipynb
#Prévoir
predict_elmo = dad_elmo.predict(ath_elmo)
predict_bert = dad_bert.predict(ath_bert)
predict_use = dad_use.predict(ath_use)
#Corriger les données de réponse
answer = test_arr[:, 0].astype(np.float)
anomaly_detection.ipynb
#Fonction d'évaluation de la précision
def calc_precision(answer, predict, title, export_fig=False):
acc = accuracy_score(answer, predict)
precision = precision_score(answer, predict)
recall = recall_score(answer, predict)
f1 = f1_score(answer, predict)
cm = confusion_matrix(answer, predict)
print("Accuracy: {:.3f}, Precision: {:.3f}, Recall: {:.3f}, \
F1: {:.3f}".format(acc, precision, recall, f1))
plt.rcParams["font.size"] = 18
cmd = ConfusionMatrixDisplay(cm, display_labels=[0,1])
cmd.plot(cmap=plt.cm.Blues, values_format='d')
plt.title(title)
if export_fig:
plt.savefig("./cm_" + title + ".png ", bbox_inches='tight')
plt.show()
return [acc, precision, recall, f1]
anomaly_detection.ipynb
#Évaluation de la précision
_ = calc_precision(answer, predict_elmo, title='ELMo', export_fig=True)
_ = calc_precision(answer, predict_bert2, title='BERT', export_fig=True)
_ = calc_precision(answer, predict_use, title='USE', export_fig=True)
Les résultats sont les suivants.
Accuracy | Precision | Recall | F1 | |
---|---|---|---|---|
ELMo | 0.916 | 0.910 | 0.923 | 0.917 |
BERT | 0.851 | 0.847 | 0.858 | 0.852 |
USE | 0.946 | 0.931 | 0.963 | 0.947 |
Matrice confuse <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/ec194af5-3dc1-836f-0739-18e3dceb6529.png ", height=200><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/a539e4d5-161d-2c87-5fc0-5da16b9780ee.png ", height=200><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/70faf102-db47-c39f-4908-d8dc529cad3f.png ", height=200>
En utilisant le seuil déterminé dans les paramètres d'apprentissage supervisé, la précision de tout modèle est grandement améliorée. En comparant les trois modèles, la précision de USE est la plus élevée de tous les indicateurs, et inversement la précision de BERT est la plus faible.
Enfin, tracez le degré d'anomalie des données de test pour chaque modèle séparément pour les données normales et les données anormales. <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/98cee787-b773-e0fe-b10c-09262fc9440c.png ", height=200><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/c57dbbdc-4b5e-06cf-d542-b6b35c5344e5.png ", height=200><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/fe357b32-518e-58b8-07eb-fd1e2c74bb70.png ", height=200> La ligne pointillée verticale rouge représente le seuil obtenu en utilisant l'étiquette de réponse correcte. C'est un seuil déterminé à partir des données de développement qui ne contient que 1% de données anormales, mais on voit qu'il se situe certainement à l'intersection de la distribution des données normales et des données anormales. En ce qui concerne le BERT, on peut voir que la distribution des données normales et des données anormales se chevauche fortement, et donc la précision est la plus faible.
Comme il n'était pas possible de définir correctement le seuil du degré d'anomalie avec le paramétrage de l'apprentissage non supervisé comme dans l'article précédent, nous avons créé un modèle de détection d'anomalies avec un enseignant. Même s'il y a un enseignant, l'étiquette de réponse correcte n'est utilisée que pour déterminer le seuil du degré d'anomalie, c'est donc la qualité du modèle de codeur qui détermine la précision, c'est-à-dire dans quelle mesure la distribution du degré d'anomalie des données normales et des données anormales est séparée. Cela dépendait de ce que je pouvais faire. En conséquence, le modèle d'encodeur le plus précis était USE. Est-ce que la tâche liée à la similitude cosinus est toujours ce à quoi USE est bon?
Recommended Posts