運用の属人化脱却に向けて -ソフトウェアのリリースに含まれる内容を機械的に扱う(doc2vecでZabbixのissueの内容をベクトル化)

保守・運用の作業を行うにあたって、様々な情報を元として作業を行う・行わないを判断しているかと思います。
その判断内容が特定の人の経験や頭の中にあるだけだと属人的になり、運用に人手をかけて対応しないといけない状況からの脱却ができません。

そこで、少しでも運用作業の現場で行っていることを機械的に扱える状態に向けて進めたいと思います。

今回はタイトルの通り、何かソフトウェアの新バージョンがリリースされた時に、それを適用すべきか否かの判断のため、リリースに含まれる内容がどういったものかを機械的に扱える状態にしてみます。
ゴール(ソフトウェアのリリースを適用するしないの自動判断)にはまだまだ遠いですが、まず第1歩として、各バージョンに含まれる対応チケットの情報を分析し、各リリースがどういった特徴のものであるかをデータ化してみます。
(機械学習とかはかなりの初心者なので、誤り等あればご指摘ください。。)

# やったこと

今回試したことは、例として、Zabbixの対応チケットの内容を分析し、各バージョンの内容をベクトル化してみます。
分析にはPythonの[gensim](https://radimrehurek.com/gensim/)ライブラリに含まれるDoc2vecを利用しました。

この辺りの記事を参考にさせていただきました。

* [Doc2Vecの仕組みとgensimを使った文書類似度算出チュートリアル - DeepAge](https://deepage.net/machine_learning/2017/01/08/doc2vec.html)
* [口コミのdoc2vecを用いたリコメンデーションシステムの構築 - Qiita](https://qiita.com/bokeneko/items/36a549b1eb3f22a58c3f)

# データの収集 (Zabbixのチケット情報の収集)

Zabbixは開発チケットがJIRAで管理されています。まずはここから対応チケットの情報をクローリングして取得します。取得にはpythonのjiraライブラリを使っています。
フォーマットとしてはひとまずこんな感じで。

```
チケットID<tab>チケットタイトル<tab>チケット概要<tab>修正バージョン(/区切りで複数)
```

実際のデータとしてはこんな感じです。

```
ZBXNEXT-4551	Improve API of Zabbix with Node.JS	Helo guys, I want propose ・・・略・・・this development. see ya	4.0 (plan)
```


```py:zabbix-jira.py
from jira import JIRA

options = {
                'server': 'https://support.zabbix.com',
                }
jira = JIRA(options)
projects = jira.projects()

issues = jira.search_issues("", 0, 100)

for issue in issues:
        fixversion_list = []
        for fixversion in issue.fields.fixVersions:
                fixversion_list.append(fixversion.name)
        fixversion_str = "/".join(fixversion_list)
        description = ""
        if issue.fields.description:
                description = issue.fields.description.replace('\n','')
                description = description.replace('\r','')
        print("%s\t%s\t%s\t%s" % (issue.key, issue.fields.summary, description, fixversion_str))
```

実行例は以下のような感じです。

```shell-session
$ python zabbix-jira.py > zabbix-issue.tsv
```

上記の例は直近100件のみ取得する例です。
今回以降の処理のために、少しずつ取得して、合計16,932件のチケットデータを収集しました。
(試す場合には負荷をかけないようご配慮ください。Zabbixさんクローリングかけてすいません。)

# doc2vecで各チケットの内容をベクトル化してみる

次に、得られた各チケットの情報を単語で分割し、文章全体をベクトル化してみます。

```py:issue-analyze.py
from gensim.models.doc2vec import Doc2Vec
from gensim.models.doc2vec import TaggedDocument

import csv
file = open("./zabbix-issue.tsv", "r")
csv.field_size_limit(1000000000) # デフォルトだとcsvライブラリで扱える最大のフィールドの文字数サイズ制限にひっかかるので最大値引き上げ
f = csv.reader(file, delimiter="\t", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)

trainings = []
for i,line in enumerate(f):
    target_str = line[1] + " " + line[2]  #チケットタイトルとチケット本文をとりあえずまとめて1つの文章として扱ってみる
    trainings.append(TaggedDocument(words = target_str.split(), tags = [line[0]])) # tagとしてはチケットのIDを渡す

m = Doc2Vec(documents= trainings, dm=1, vector_size=300, epochs=10, window=5, min_count=1, workers=4)
print(m.docvecs.most_similar(0))
```

求めるベクトルのサイズは300次元ベクトル、文脈を扱うための単語数を5単語、1件でも現れる単語は分析の対象とする、1つの訓練データを10回学習というパラメータで実施です。

これで実行してみるととりあえず0番目のチケット(ZBXNEXT-4551)に類似するチケットの10件が以下のように算出されます。

```shell-session
$ python issue-analyze.py
[('ZBXNEXT-3602', 0.7806828618049622),
 ('ZBX-4501', 0.7637400031089783),
 ('ZBXNEXT-1106', 0.7404146194458008),
 ('ZBX-369', 0.7393386363983154),
 ('ZBX-4535', 0.7377630472183228),
 ('ZBX-9327', 0.7305757999420166),
 ('ZBXNEXT-1158', 0.7287787795066833),
 ('ZBXNEXT-3300', 0.7266151309013367),
 ('ZBXNEXT-2136', 0.7238351106643677),
 ('ZBX-3057', 0.7228463888168335)]
```

この一番スコアが高くでているチケットの内容を見ると

```
ZBXNEXT-3602  history.get API method doesn't work  Hello. We have a problem with API. history.get doesn't return anything. We use user with zabbix admin permission for API request. I've attache screenshots from wireshark. Could you please have a look? Thanks.
```
ZabbixのAPIに関する問い合わせの記述であることがわかります。
0番目のZBXNEXT-4551のチケット内容は以下です。
こちらはZabbix APIに関する提案のチケットです。なんとなく挨拶文とか変な要素が類似度に影響しているような感じも受けます。。この辺は前処理でうまく除くとか対処が必要かも。

```
ZBXNEXT-4551 Improve API of Zabbix with Node.JS Helo guys, I want propose we development a new API with nodejs separated from zabbix server. For we granted more perfomance.I am willing to the help this development. see ya
```

ちなみに他のチケット同士の類似度として、あるチケットの対応が別チケットに置き換えられている以下のチケットの場合、どれぐらいの類似度が出るかを確認してみると0.68ということでそれなりのスコアは出るようです。

```
m.docvecs.similarity("ZBX-12589","ZBX-11675")
0.6872035807505558
```

```:ZBX-12589
Zabbix server crash [signal:11(SIGSEGV),reason:1,refaddr:(nil)] Please find crash information from log file:{noformat} 11166:20170824:024722.204 [Z3001] connection to database 'zabbix' failed: [2002] Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2) 11166:20170824:024722.204 Got signal [signal:11(SIGSEGV),reason:1,refaddr:(nil)]. Crashing ... 11166:20170824:024722.204 ====== Fatal information: ====== 11166:20170824:024722.204 Program counter: 0x7ff73426c452 11166:20170824:024722.204 === Registers: === 11166:
・・・略
```

```:ZBX-11675
Zabbix server crashing due to long query getting inserted into trends_unit {noformat} 3379:20170105:114152.752 End of zbx_vc_get_value_range():SUCCEED count:1 cached:1  3379:20170105:114152.752 End of evaluate_LAST():SUCCEED  3379:20170105:114152.752 End of evaluate_function():SUCCEED value:'16.5399'  3379:20170105:114152.752 In evaluate_function() 
・・・略
```

# リリースの特徴を各チケットの特徴ベクトルの加算平均でベクトル化

各チケットの特徴がここまでの処理でそれぞれ300次元ベクトルとして得られたので、ここからは各リリースがどういう特徴かを計算してみます。

ここからはこれが正しいやり方なのかはわかっていないですが、各リリースに含まれるチケットの特徴ベクトルを加算平均して1つの300次ベクトルを導きます。

Zabbixのチケットに記載のfixVersionの情報でチケットを分類し、引数で指定したバージョンに合致するチケットのベクトルを各要素ごとに合算し平均値を求めます。ベクトル計算にはnumpyのライブラリを使ってかんたんに実施。

```py:calcurate-release-vec.py
from gensim.models.doc2vec import Doc2Vec
from gensim.models.doc2vec import TaggedDocument

import csv
import sys
import numpy as np

if len(sys.argv) != 2:
    print("argument error: command example) calcurate-release-vec.py 3.2.0")
    quit()

version = sys.argv[1]

file = open("./zabbix-issue.tsv", "r")
csv.field_size_limit(1000000000)
f = csv.reader(file, delimiter="\t", doublequote=True, lineterminator="\r\n", quotechar='"', skipinitialspace=True)

release_issues = np.empty((0,300), "float32")

release_issue_num_list = []

for i,line in enumerate(f):
    if len(line) >= 4 and version in line[3]:
        release_issue_num_list.append(line[0])

m = Doc2Vec.load("./zabbix-issue-doc2vec.model")
for issue_num in release_issue_num_list:
    release_issues = np.append(release_issues, [m.docvecs[issue_num]], axis=0)

print(np.average(release_issues, axis=0))
```
なお、上記では改めてモデルは作り直さず、gensimのDoc2Vecのsaveメソッドを使ってモデルを保存した内容をloadして処理実行しています。

実行するとこんな感じです。

```shell-session
$ python calcurate-release-vec.py 4.0.0
[ 0.07120704  0.13458145  0.04182658 -0.03798934 -0.01006003  0.00297882
  0.03397377 -0.08053464 -0.00791342  0.09081631 -0.11359077 -0.02950951
  0.02545199  0.01095048  0.04909791 -0.08181744 -0.06762014 -0.00111464
 -0.01757004  0.03285154  0.03573884  0.03162723  0.01588897  0.00327088
 -0.02299169 -0.02235058 -0.13335058  0.06926978 -0.0785749   0.06585309
・・・略
 -0.06656372 -0.01710121  0.02639024  0.03763243  0.07340473 -0.02144853
 -0.00705793 -0.10957732 -0.08544407 -0.05307537 -0.0086156   0.00660839
 -0.06605044 -0.02172687  0.01978751  0.0586208  -0.06232915 -0.0194738
  0.03509236  0.0681367   0.05907284 -0.01087932 -0.0663033   0.0531706
 -0.13275819  0.05441188  0.0458594   0.04902027 -0.04066626 -0.00254502
 -0.04562102 -0.06445721  0.02657518  0.04935394 -0.1185377  -0.04649137]
```

これが4.0.0のリリースを示す特徴ベクトルという感じです。

# まとめ
データの前処理で不要な文言削除したり、Doc2Vecのモデル作成時のパラメータをチューニングしたりしないと精度悪そう、かつ今回はチケットタイトルと概要のみを取り扱っているため、全体的に1チケット当りの単語数が少ないため精度でなさそうですが、保守運用時に参考にしているようなテキスト情報を分析可能な数値ベクトルの形態に変換する1つのやり方を試してみました。
他にも[この記事](https://qiita.com/shima_x/items/196e8d823412e45680e9)のようにクラスタリングしたり、分析の仕方は色々ありそうです。