[PYTHON] Détecter les anomalies dans les phrases en utilisant ELMo, BERT, USE

Aperçu

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.

environnement

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

Préparation des données

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.

Installation du dictionnaire MeCab + NEologd

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)

Développement d'un modèle de détection d'anomalies

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 $ $ 1- \ alpha = \ int_0 ^ {a_ \ text {th}} \\! Dx \, \ chi ^ 2 (x | \ hat {m}, \ hat {s}) En résolvant $, le seuil $ Déterminé a_ \ text {th} $. Cette méthode est implémentée dans la méthode 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.

    1. Le degré d'anomalie des données de développement est divisé entre ceux des données normales et ceux des données anormales.
  1. Ajustez chacune des deux distributions d'anomalie avec une distribution du chi carré.
  2. Soit l'intersection des deux distributions du chi carré le seuil du degré d'anomalie. La figure ci-dessous montre le résultat du suivi de cette procédure pour le degré d'anomalie calculé par USE. Les lignes noires et bleues pleines représentent les deux distributions du chi carré et la ligne rouge en pointillé représente le seuil. <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613488/d05ab151-a0a6-b681-8769-9704e6c9c536.png ", height=240> Cette procédure de recherche du seuil est implémentée dans la méthode 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.

en conclusion

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

Détecter les anomalies dans les phrases en utilisant ELMo, BERT, USE
Utilisez MeCab pour traduire des phrases bâclées de manière "lente".
Comment utiliser les classes dans Theano
Mock in python - Comment utiliser mox
Comment utiliser SQLite en Python
Comment utiliser ChemSpider en Python
Comment utiliser PubChem avec Python
Utilisez ELMo et BERT pour déterminer la similitude des mots pour les mots polynomiaux
Comment utiliser les colonnes calculées dans CASTable
[Introduction à Python] Comment utiliser la classe en Python?
Comment utiliser Google Test en langage C
Un moyen simple d'utiliser Wikipedia avec Python
Connaissances minimales pour utiliser Form avec Flask
Comment utiliser l'interpréteur d'Anaconda avec PyCharm
Comment utiliser __slots__ dans la classe Python
Comment utiliser les expressions régulières en Python
Comment utiliser Map dans ViewPager d'Android
Comment utiliser is et == en Python
Comment utiliser la bibliothèque C en Python
Comment utiliser la bibliothèque d'images Python dans la série python3
EP 11 Utiliser `zip` pour traiter les itérateurs en parallèle
Utilisez un module de cryptographie qui gère OpenSSL en Python
Comment utiliser tkinter avec python dans pyenv
Connectez-vous avec json en utilisant pygogo.
Utilisez os.getenv pour obtenir des variables d'environnement en Python
[Explication de la mise en œuvre] Comment utiliser la version japonaise de BERT dans Google Colaboratory (PyTorch)
[Pour les débutants] Comment utiliser la commande say avec python!
Je veux utiliser self avec Backpropagation (tf.custom_gradient) (tensorflow)
Comment utiliser le bootstrap dans la vue de classe générique Django
Comment utiliser le moteur de modèle dans une application de fichier de Pyramid
Comment utiliser la clause exist dans l'ensemble de requêtes Django
Comment utiliser des variables dans les fichiers de définition d'unité systemd
Pratique pour utiliser les sous-graphiques matplotlib dans l'instruction for
J'ai essayé de résumer comment utiliser les pandas de python
Comment utiliser le modèle appris dans Lobe en Python
Comment utiliser Decorator dans Django et comment le créer
Utilisez la date sur l'axe des x du tsplot représenté dans seaborn
Comment utiliser le modèle japonais Spacy avec Google Colaboratory
Je veux utiliser le jeu de données R avec python