SBTのスベテ

膨大なメールを深層学習して運用効率化ツールを作る5

前川 敦史

前川 敦史

インフラやアプリのシステム運用を主の業務としております 前川敦史です。
膨大なメールを深層学習して運用効率化ツールを作る方法紹介の続きとなります。

前回のエピソードで記載させて頂いた1人で撮影したプリクラについてですが、社内で自慢したいので常にプリクラを持ち歩きながら仕事をしておりましたら、同僚の机の上に置き忘れてしまっておりました。
社内で落としたと勘違いし探しまわっておりましたら、後日同僚が私の元に直接届けてくれました。
もし私を知らないかたに拾われていたら、弊社の社内 Web ポータル上の落とし物リストに掲載されるところでした。

社内 Web ポータルに掲載された場合のイメージ
※もし、社内 Web ポータルに掲載された場合のイメージ

これはこれで社内全体に自慢できるのでよかったのかもしれません。

では、運用効率化ツールのお話に戻りたいと思います。
今回で ”膨大なメールを深層学習して運用効率化ツールを作る” は最終回となります。
これまでの記事では動作させる事を優先し、中身については触れておりませんでしたが、中身に触れつつ、カスタマイズする方法をご紹介したいと思います。

当社のご提供する「機械学習導入支援サービス」資料請求・お問い合わせはこちら


1. 学習部分

膨大なメールを深層学習して運用効率化ツールを作る3」にて公開しました sbt_outlook_doc2vec.zip ファイルの中にございますスクリプトの解説及びカスタマイズ方法となります。


a. 学習文章から必要な品詞のみを抽出する

MeCab で学習を行う際に必要な品詞のみを選択し、精度をあげていく事ができます。
MeCab で利用されている IPA 品詞体系という表を参考にできるだけ必要な単語を出せるようにします。


対象としたい品詞
learning.py 内の58行目にて対象としたい品詞を指定します。

# 対象としたい品詞 mecab_enable_class = ['動詞', '形容詞', '名詞']

感動詞、助詞、記号なども学習対象としたい場合は配列に追加してください。


除外したい品詞
learning.py 内の60行目にて除外したい品詞(詳細)を指定しています。
-(ハイフン)で Type1~4 までを指定し、前方一致で判別されます。

# 除外したい品詞 mecab_ignore_class = ['名詞-ナイ形容詞語幹', '名詞-非自立-助動詞語幹', '名詞-接尾-特殊', '名詞-代名詞-一般', …]

必要に応じて追加してください。


除外したい単語
learning.py 内の63行目にて除外したい単語を指定しています。
不要と思われる単語やよく使われる単語を除外します。

# 除外したい単語 mecab_ignore_word = ['fax', 'Tel', 'TEL', 'あたり', …]



公開したソースコードでは”これが良さそう”と思うものを入れていますが、ご自身の文章にあわせてカスタマイズする事で類似判定の精度向上が見込めます。


b. Outlook から様々な情報を取得する

learning.py 内の75行目では Outlook からメールを取得し Doc2Vec 用のデータに変換する関数である make_sentence_from_outlook を定義しております。

今回は Outlook 上のメールから件名、本文のみを取得していますが、差出人や受信日時やメールヘッダーなど、様々な情報をPythonから取得できますのでご紹介しておきます。
以下のソースコードを参考にして頂ければと思います。

#!/usr/bin/env python # -*- coding: utf-8 -*- import win32com.client PR_SMTP_ADDRESS = "http://schemas.microsoft.com/mapi/proptag/0x39fe001e" PR_TRANSPORT_MESSAGE_HEADERS = "http://schemas.microsoft.com/mapi/proptag/0x007D001E" outlook_mapi = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") # Outlook API読み込み outlook_folder = outlook_mapi.GetDefaultFolder(6) # inboxのフォルダを取得 for outlook_item in outlook_folder.Items: # アイテムをループ if outlook_item.Class != 43: continue # 通常のメールではない会議通知などを除外する print('entryid: ',outlook_item.entryid) # outlook mailitemのユニークなID print('ConversationID',outlook_item.conversationid) # メールの識別子 返信などグループ化されている内容を捜索したい時に使用 print('SentOn: ',outlook_item.senton) # 送信日時 print('ReceivedTime: ',str(outlook_item.receivedtime)) # 受信日時 print('Subject: ',outlook_item.subject) # 件名 outlook_item_mailheader = outlook_item.PropertyAccessor.GetProperty(PR_TRANSPORT_MESSAGE_HEADERS) # mail headerを取得 print('Header: ', outlook_item_mailheader) print('Size: ',outlook_item.size) # mailitemサイズ print('To: ',outlook_item.to) # 宛先 print('CC: ',outlook_item.cc) # CC print('Body: ',outlook_item.body) # 本文 print('SenderAddress: ',outlook_item.sender.address) # 送信者メールアドレス # Office365環境などでメールアドレスが正しく取得できない場合 for outlook_item_rec in outlook_item.Recipients: if outlook_item_rec.address == 'Unknown':continue mail_str_address = "" mail_address_type = outlook_item_rec.Type # どこのアドレスかを取得する 1:to , 2:cc , 3:bcc mail_str_address = outlook_item_rec.PropertyAccessor.GetProperty(PR_SMTP_ADDRESS) print(outlook_item_rec.name,mail_str_address)

今回は Outlook 上にあるメールから学習する事を前提としておりますが、別のデータソース(SQL データベースやインシデント管理ツール)から取得するコードに変える事で、Outlook 以外でもツールを作る事が可能です。

def make_sentence_from_XXXXXXXX(): for #別の情報ソースのループ yield {"sentences":TaggedDocument(words=["単語リストの配列"] , tags=["なにかしらのユニークなID"]) , "subject":"件名のようなもの"}


c. 学習部分のカスタマイズ

learning.py の171行目以降の部分で Doc2Vec が学習を行う際のオプション設定を変更できます。
学習したい文章に合わせて変更していくと精度の向上が見込めます。
詳しくはソースコード内のコメントに書いております。

epoch_logger = EpochLogger() model = models.Doc2Vec( vector_size=400, # ベクトル化した際の次元数 alpha=0.0015, # 学習率 高いほど収束が速いが高すぎると発散する。 低いほど精度が高いが収束が遅くなる。 sample=0.0001, # 単語を無視する際の頻度の閾値 あまりに高い頻度で出現する単語は意味のない単語である可能性が高い為 無視する閾値を設定する min_count=1, # 学習に使う単語の最低出現回数 sampleとは逆 頻度が少なすぎる単語は意味がない可能性が高い為 無視するその閾値を設定する workers=5, # 学習時のスレッド数 # compute_loss=True, # 学習ロスを記録するかどうか(gensim 3.7.1現在 Doc2Vecには未対応) callbacks=[epoch_logger] # 学習時のコールバック先classを指定する ) model.build_vocab(sentences) model.train(sentences, total_examples=model.corpus_count, epochs=TRAIN_EPOCHS) # 学習を開始

また、learning.py の46行目で学習する回数を変更する事ができます。
こちらも精度によって増やしたり減らしたりしながら調整していきます。

# 学習回数を指定 TRAIN_EPOCHS = 200


d. 正規化されたデータのキャッシュを作成する

learning.py の189行目についての説明となります。
私のプログラムでは速度向上のため Doc2Vec 用に用意された関数ではなく、全文章のベクトルをあらかじめ正規化(数字を変換する下準備のようなイメージ)したものをキャッシュとして保存しております。
matutils.unitvec という関数で全文章のベクトルの正規化を行い、pickle.dump という関数でファイルに書き出しています。
Doc2Vec の関数で行う場合は以下の行は不要となります。

# ------------------------------------- # 正規化済のデータをキャッシュさせる # ------------------------------------- print('doc2vec 正規化済のデータを保存') model_l2norm_list=[] for model_words,model_tags in sentences: r2=matutils.unitvec(array([model[doc] for doc in model_words]).mean(axis=0)) model_l2norm_list.append(r2) with open(DATA_DIR+'/model_l2norm_list.pickle', mode='wb') as f: pickle.dump(model_l2norm_list, f, protocol=4)


e. embedding projector用のデータ作成

learning.py の200行目以下ですが embedding-projector にて可視化する為のファイルが gzip で圧縮した形で入っています。

# ------------------------------------- # embedding-projector用データ(word)を保存 # ------------------------------------- print('embedding-projector用データ(word)を保存') word_file_metadata=gzip.open(DATA_DIR+"/word_metadata.tsv.gz", 'wt',encoding='utf-8') word_file_tensor=gzip.open(DATA_DIR+"/word_tensor.tsv.gz", 'wt',encoding='utf-8') word_file_metadata.write('word\t読み\t基本形\t品詞\t品詞2\n') for i,word in enumerate(model.wv.index2word): vec = '\t'.join([str(x) for x in model.wv[word]]) word_mecab_line=mecab.parse(word).split("\t") if len(word_mecab_line) < 3: continue word_mecab_line[4]=word_mecab_line[4].replace("\t","") if word_mecab_line[4]=="":word_mecab_line[4]="-" word_file_metadata.write(word+"\t"+word_mecab_line[1]+"\t"+word_mecab_line[2]+"\t"+word_mecab_line[3]+"\t"+word_mecab_line[4]+"\n") word_file_tensor.write(vec+"\n") word_file_metadata.close() word_file_tensor.close() # -------------------------------------- # embedding-projector用データ(doc)を保存 # -------------------------------------- print('embedding-projector用データ(doc)を保存') doc_file_metadata=gzip.open(DATA_DIR+"/doc_metadata.tsv.gz", 'wt',encoding='utf-8') doc_file_tensor=gzip.open(DATA_DIR+"/doc_tensor.tsv.gz", 'wt',encoding='utf-8') doc_file_metadata.write("subject\tmailid\n") for offset in range(len(model.docvecs.offset2doctag)): tag = model.docvecs.offset2doctag[offset] vec = '\t'.join([str(x) for x in model.docvecs[tag]]) doc_file_metadata.write(subjects[offset]+"\t"+model.docvecs.offset2doctag[offset]+"\n") doc_file_tensor.write(vec+"\n") doc_file_metadata.close() doc_file_tensor.close()

このコードにより出力されるファイルです。

左右にスクロールしてご覧ください。

word_tensor.tsv.gz 単語のベクトルデータが入ったファイルです。各単語の位置情報を表す数値が入っています。
word_metadata.tsv.gz 単語の情報詳細が入ったファイルです。以下のようなデータが入っています。
word 読み 基本形 品詞 品詞2
ソフトバンク ソフトバンク ソフトバンク 名詞-固有
名詞-組織
-
doc_tensor.tsv.gz 文章のベクトルデータが入ったファイルです。各文章の位置情報を表す数値が入っています。
doc_metadata.tsv.gz 文章の情報詳細が入ったファイルです。メールタイトル[tab]メール ID が入っています。

こちらの行も embedding-projector による可視化を行わない場合は不要となります。


2. 表示部分

続いて「膨大なメールを深層学習して運用効率化ツールを作る4」にて公開致しました、sbt_outlook_doc2vec_runserver.zip ファイルの中にございますスクリプトの解説及びカスタマイズ方法となります。


a. 基本的な設定

runserver.py の33行目の部分で返す文章/単語の数と、立ち上げるサーバーポートを指定できます。

# ---------------------------------------------- # 設定 # ---------------------------------------------- RESULT_TOPN = 10 # Flaskで類似判定の結果を返す単語or文章の数 SERVER_PORT = 8000 # Flaskで立ち上げるサーバーポート


b. Web サーバ=Flask サーバ立ち上げ+学習済みデータの読み込み

runserver.py の39行目以降です。
Web サーバである Flask の立ち上げと学習時に作成されたファイルを読み込んでいます。
このあたりの行でエラーが出ましたら学習の際のモデル作成に失敗している可能性があります。
膨大なメールを深層学習して運用効率化ツールを作る3」で掲載した学習モデル作成を再度実行し、エラーが発生していないかを確認いただければと思います。
変数:model には Doc2Vec の機能で作成した学習モデルデータが読み込まれます。
変数:subjects には全メールの件名が配列の形で読み込まれます。
変数:model_l2norm_list には全メールのベクトルを正規化したデータが配列の形で読み込まれます。

# ---------------------------------------------- # Flask+各学習ファイル読み込み # ---------------------------------------------- # Flask立ち上げ app = Flask(__name__) # doc2vec学習モデルの読み込み model = models.Doc2Vec.load(DATA_DIR+'/model') # 件名リストの読み込み with open(DATA_DIR+'/subjects.pickle', mode='rb') as f: subjects = pickle.load(f) # 正規化済み変数の読み込み with open(DATA_DIR+'/model_l2norm_list.pickle', mode='rb') as f: model_l2norm_list = pickle.load(f)

237行目でサーバの起動そのものが行われます。

app.run(port=SERVER_PORT, threaded=True, debug=False)


c. Flask 経由でユーザーのリクエストを受け取る方法

runserver.py の56~230行目で Web サーバが応答する部分を書いています。
例えば以下の59行目部分の場合ですと、Web サーバに対し、"/word/" の GET or POST リクエストが来た場合に get_request_word という関数が呼び出されるようになっています。

@app.route('/word/', methods=['GET', 'POST']) def get_request_word(): …


d. 類似単語を返す関数

get_request_word 関数の中の75行目の部分で”学習データから類似した単語を返す”という関数が呼び出されます。
Doc2Vec のモデルデータを使用しまして model.most_similar 関数を使用する事で、近い単語のリストを受け取る事ができます。
王様(positive) - 男(nagative) + 女(positive) = 女王 というような結果を返す事ができます。

result_w2v_list = model.most_similar( positive=req_dict['positive'], negative=req_dict['negative'], topn=int(RESULT_TOPN) )

model.most_similar で抽出した類似単語を JSON 形式で返しています。


e. 類似した文章を返す関数

runserver.py の94~130行目で文章を返す部分を定義しています。
/doc/ というリクエストが来た際に get_request_doc 関数が呼び出されます。
Web リクエストの text というクエリ入った未知の文章に類似した文章の件名とメール ID を返す関数となります。

@app.route('/doc/', methods=['GET', 'POST']) def get_request_doc(): …

以下100行目では
学習時にも使用していた以下の関数 mecab_sentence_to_words で未知の文章から単語のみを抽出します。
learning.py の関数を呼んでいるので、学習時と同じルールで単語化されています。

words = mecab_sentence_to_words(req_dict['text']) # 文章をMeCabにかけて単語リスト化する(learning.pyに定義された関数)

以下103行目の部分で
学習データが知っている単語
学習データが知らない単語
に分けています。

words_known = [word for word in words if word in model.wv.vocab] words_unknown = [word for word in words if not word in model.wv.vocab]

以下112~120行目の部分で
”words_known(単語の集合体)から score(類似度合いを表す値)を出し 類似度が高い順に並び替えて返す” という事を行っています。
score と近い順に並べられたメールの件名、ID が格納された変数=results_vecs を作成し、それを JSON 形式で返しています。

v1 = [model[doc] for doc in words_known] # 単語の集合体のlistをベクトルデータに変換させる r1 = matutils.unitvec(array(v1).mean(axis=0)) # wordsベクトルの集合体からdoc2vecのvecに変換し正規化させる # 学習時に計算して保存していた全文章の正規化済みデータと要求された文章の正規化データを比較して 類似度を計算する score_dic={} for offset in range(len(model_l2norm_list)): score_dic[offset]=dot(r1,model_l2norm_list[offset]) # 内積を結果(score)として格納していく sorted_arr=sorted(score_dic.items(),key=lambda x:x[1], reverse=True) # scoreを降順にソートする # レスポンス用にスコアを文章情報を格納していく for k,v in sorted_arr[0:RESULT_TOPN]: results_vecs.append({"offset":str(k),"score":str(v),"mailid":model.docvecs.offset2doctag[k],"subject":subjects[k]})

Doc2Vec(gensim version 3.7.1 現在)では未知の文章から類似文章を直接抽出する関数が用意されていませんが、以下のような仕組みで未知の文章から類似文章の抽出を行っています。

  1. 未知の文章から単語を抜き出して、学習データから単語ごとのベクトルを抽出
  2. 単語ごとに抽出されたベクトルを正規化
  3. 全文章の正規化済ベクトルキャッシュデータから2で作成した値を比較して類似度(score)を抽出
  4. 類似度(score)が高いもの順に並び替え、類似した文章順として返す

(注:未知の単語が含まれている場合は無視して計算をしています)
学習データを読み込んだ状態で Web サーバが起動しているので早いレスポンスが期待できます。
また、この仕組みは Azure WebApp , Azure Functions でも動作する事を確認しております。


f. Web サーバからそのままファイルを返す関数

runserver.py 192行目部分で要求されたファイルをそのまま返す関数を定義しています。
ファイルごとに MineType を指定してファイルが存在すればそのファイルを読み込んで Web 経由で返すようにしています。
embedding-projector 用に圧縮された gzip ファイルのロード用に215-224行目で工夫を加えています。

def static_file(path): ... ... # --------------------------- # gzip用のレスポンス # --------------------------- if ext == '.gz': with open(path,mode="rb") as f: content = f.read() res = Response(content) res.headers["Content-Type"] ="text/plain" res.headers["Content-Encoding"] ="gzip" return res

当社のご提供する「機械学習導入支援サービス」資料請求・お問い合わせはこちら


3. WebUI について

sbt_outlook_doc2vec_runserver.zip ファイルの中にございます webui フォルダの中身についてです。
Flask で作られた WebAPI から JSON を取得して表示するという事のみを行っています。(Bulma という CSS framework 使用しています)
”例えば… Doc2Vec でこんな検索ツールができるよ” というイメージを持っていただければと思い作りました。

検索ツールのイメージ


4. WordCloud 用データ作成について

Doc2Vec の学習モデルデータから以下のような単語出現頻度を可視化する事が可能となります。

単語出現頻度を可視化

以下は sbt_outlook_doc2vec_runserver.zip 内にございます doc2vec_to_wordcloud.py の中身全てとなります。
上記では SBT という字に書き込んでおりますが、こちらは WORDCLOUD_MASKPNG で別の画像を指定する事で変更が可能です。
STOP_WORDS に足す事で表示を除外したい単語を指定できます。

#!/usr/bin/env python # -*- coding: utf-8 -*- # ----------------------------------------------- # doc2vecのデータからwordcloudのデータを作成します # ----------------------------------------------- import pickle import numpy as np from PIL import Image from os import path from wordcloud import WordCloud from gensim import models # マスク処理に使用する画像パス WORDCLOUD_MASKPNG = 'sbt.png' # 除外する単語 STOP_WORDS = ["なる","する","できる","よう","いく","よう","もの","れる","ある","せる","くれる","こと","いる","ない","ため","致す",'くださる',"つく","いたす","いただく","頂ける","申し上げる","掛ける","存じる","ござる"] model = models.Doc2Vec.load('data/model') word_list = {} for v in model.wv.vocab: word_list[v]=model.wv.vocab[v].count maskimg = np.array(Image.open(path.join(WORDCLOUD_MASKPNG))) wc = WordCloud( background_color="rgba(0, 0, 0, 0)", mask=maskimg, mode="RGBA", font_path=r"C:/WINDOWS/Fonts/meiryo.ttc", width=3832, height=2736, max_words=5000, max_font_size=120, min_font_size=4, stopwords=set(STOP_WORDS) ).generate_from_frequencies(word_list) wc.to_file("wordcloud.png") print("作成完了")

Doc2Vec の学習モデルデータで用意されている "model.wv.vocab" を利用する事で学習された文章に含まれる全単語とその単語が使用された数を取得できるため この仕組みを利用して WordCloud を簡単に作成できます。
不要な単語の確認などに使用できると思いますので Doc2Vec をご利用の際は是非一度試して頂きたいです。
既に前回の記事で公開してしまっているので諦めますが、”ハンマーカンマー” という文字を STOP_WORDS に入れるべきだったと後悔しています。


5. まとめ

膨大なメールを深層学習して運用効率化ツールを作るについての記事は以上となります。
運用効率化を担当としている方々に機械学習の入口となるようなモノをお伝え出来れば…と思いかなり具体的なレベルにかみ砕いて公開させて頂きました。
次回以降も同じように小話をはさみながらかなり具体的に実装可能な形で公開していきたいと思います。

思えば私も「AI で運用効率化できないか?」という上司からお題を頂き、四苦八苦しました。
今では様々な取り組みを行う事ができ、あのお題をくれた上司に感謝しています。


関連ページ

当社のご提供する「機械学習導入支援サービス」資料請求・お問い合わせはこちら
膨大なメールを深層学習して運用効率化ツールを作る1
膨大なメールを深層学習して運用効率化ツールを作る2
膨大なメールを深層学習して運用効率化ツールを作る3
膨大なメールを深層学習して運用効率化ツールを作る4

お問い合わせ

製品・サービスに関するお問い合わせはお気軽にご相談ください。

ピックアップ

セミナー情報
クラウドエンジニアブログ
clouXion
メールマガジン登録