フューチャー技術ブログ

社内ヘルプデスクをAIで!

背景

  • 社内ヘルプデスク(Redmine)における管理対応の業務を効率化し、サービスレベルを上げたい
  • 現状の課題
    • 起票されたチケットの解決にかかる時間が長い
    • 原因の1つは、正しい担当者にチケットが割当てられず滞留することがあること
      • 処理されないチケットは、カテゴリが正しく設定されていないものが多かった
      • 弊社の運用としてカテゴリ単位で専門的な担当者が割り当てられているので、カテゴリを間違うとやり取りが増え、解決までに時間がかかってしまいます

Redmineについては下記を参照下さい
http://redmine.jp/overview/

作ったもの

Redmineにチケットが新規に起票すると、過去のデータから自動的にカテゴリを設定する仕組みをDeepLearningを用いて作成しました。
これにより正しい担当者にチケットが割り当てられ、チケットの平均解決時間の向上を狙います。
この仕組に対して、親しみを持たせたいということで あいちゃん と命名しました。

例えば下記のような動きです(※社内情報に触れそうなところは隠しています)

さらに、カテゴリが自動設定されたことに驚かないよう、振り分けた旨のコメントもセットで投稿するようにしました。

万が一、間違った振り分けをしても許してもらえそうな新人さんキャラクターを演出しています。
今のところクレームは届いていないのですが、彼女の貢献も大きいと思います。

採用技術

  • Python パッケージ
    • conda (4.3.11) # Pythonのパッケージ管理
    • python-redmine (2.0.2)
    • Keras (2.0.5)
    • tensorflow (1.2.0)
    • Janome (0.2.8) # 形態素解析
  • Ruby パッケージ
    • faraday(0.13.0) # HTTP client library
  • ジョブ系
    • Jenkins (2.7.4)
  • ミドル
    • Docker (17.03.1-ce)

処理の流れと構成

チケットの自動カテゴリ設定の処理フローです。

  1. Redmineにチケットが起票される
  2. Redmineのweb hook pluginを使ってJenkinsジョブを呼び出す
  3. JenkinsはKeras Dockerコンテナを起動
  4. Kerasでチケットのカテゴリを判定を行う
  5. カテゴリの判定結果をRedmineのWeb API経由でチケットを更新
  6. カテゴリに紐付いたヘルプデスク担当者に、Redmine経由で通知がなされる

あいちゃんの実体は、Dockerコンテナ上のKeras(Tensorflow)+ 連携用のRubyスクリプトです。
ユーザからはRedmineのカテゴリが、あいちゃんユーザから更新されたかのように見えます。

あいちゃんを作成

まずは、 あいちゃん のコアとなるAI部分を開発します。

今回はKerasライブラリを使います。
Kerasで学習&判定させるときに必要なフローは以下の1~3です。

  1. 教師データを準備する
  2. モデルを用意する
  3. 学習させる
  4. 判定させる

1. 教師データの作成

元となるデータは運用中のRedmineが利用するDBに蓄積されている3000件のデータです。

まずは教師データを作成します。
教師データとは、入力データとそれに対応した正解データ(バイナリ)のタプルです。

$$ 教師データ = (X(入力データ), Y(正解データ)) $$

今回は、「チケットの件名」と「チケットの内容」の文字列からカテゴリを出したいので、以下の形式です。

$$ X(入力データ) = チケットの件名 + 内容の文字列 $$ $$ Y(正解データ) = カテゴリ $$

入力データ(X)と正解データ(Y)の作成フローを下図にまとめました。

サンプルコード.py
# 題名と本文を結合
text = subject + description

# 文字列を形態素解析して、名詞、品詞等にわける
tokenizedTexts = JanomeTokenizer().tokenize(text)

# 出現頻度で数字に変換し、配列化
tokenizer = KerasTokenizer()
seq = tokenizer.fit_on_texts(tokenizedTexts)

# 配列のパディング
X = sequence.pad_sequences(seq)

# カテゴリIDを1次元配列に変換
# 実はKerasで教師データを作ると、バイナリデータしか扱えないため、ここで変換します。
Y = np_utils.to_categorical(categoryId)
#例:
#1 → 1000000000000
#13 → 0000000000100

2. モデルを用意する

CNN、RNN、LSTMで技術検証を行った結果、最も正解率が高かったCNNを採用しました。
CNNを利用した実装は下記のようなイメージになります。

モデルサンプル.py
model = Sequential()

# 入力層
model.add(Embedding(max_features,
embedding_dims, #50
input_length = maxlen,
dropout = 0.2))

# Convolution1D層
model.add(Convolution1D(nb_filter = 250,
filter_length = 3,
border_mode = "valid",
activation = "relu",
subsample_length = 1))

# GlobalMaxPooling1D層:
model.add(GlobalMaxPooling1D())

# 隠れ層
model.add(Dense(hidden_dims))
model.add(Dropout(0.2))
model.add(Activation("relu"))

# 出力層
model.add(Dense(10))
model.add(Activation("sigmoid"))

# 学習過程の設定
model.compile(loss = "categorical_crossentropy",
optimizer = "adam",
metrics = ["accuracy"])

3. modelで学習させる

教師データを設定します。
Kerasを用いると、モデルを作成後、fix functionX(入力データ) , Y(正解データ) , epoch数を渡すだけで学習できます。
※poch数とは、学習をさせる回数です。この回数を増やすと限界はありますが重みづけが最適化されていきます。

教師データの設定&学習サンプル
model.fit(X, Y, epoch...)

4. 判定する

学習は終わっているので、あとはX(入力データ)を与えると判定できます!
また、後続で使うRedmine操作用に、結果を加工しておきます。

カテゴリ判定サンプル
result = model.predict(predictX)

dfPredicts = pds.DataFrame(retPredict, columns = uniqueCategoryIds)
issues = {}
for (i, dfPredict) in dfPredicts.iterrows():
# 可能性が高いものから順から4つだけを取り出します。
sPredicts = dfPredict.sort_values(ascending = False).nlargest(4)
# このredmine操作で使うので、issueオブジェクトに結果を格納します。
issue = {
"subject": subject,
"category_names": [],
"category_ids" : [],
"confidences": []
}
for (j, sPredict) in enumerate(sPredicts):
category_name = re.split('\[|\]| ',sPredicts.index[j])[2]
category_id = re.split('\[|\]| ',sPredicts.index[j])[1]
issue["category_names"].append(category_name)
issue["category_ids"].append(int(category_id)),
issue["confidences"].append(sPredicts[j])

Redmineのチケットを更新

続いて、Redmine APIを使って、対象のチケットを更新します。

Python-RedmineとAPIキーを使って、対象のチケットを新しいカテゴリIDで更新します。
これをコンテナの最後の処理に差し込めば、Redmineのチケットが更新されます。

Redmineチケット更新サンプル.ruby
from redminelib import Redmine

redmine = Redmine('http://localhost/redmine', key='***********************')
issue = redmine.issue.get(issue_id)
issue.category_id = category_id
issue.save()

以下を参考にしました

Jenkinsジョブの作成

今回はRedmineから直接Kerasコンテナを呼ばずに、間にJenkinsを経由させるアーキテクチャになっています。
そのため、DockerコンテナをキックするJenkinsジョブを作成します。

サンプルコマンド
# KerasコンテナがいるジョブサーバにSSHで接続して、コンテナにticket_idを渡します。
# issue_idはparameter付きビルドでredmineから受け取ります。
$ ssh jobserver docker exec -t keras_container python3 update_issue.py $issue_id

以下を参考にしました

Redmine Pluginの作成

RedmineとJenkinsを連携させる部分を作ります。
Redmineにチケットが起票されたイベントをトリガーにしてJenkinsジョブを呼び出します。

サンプル実装.rb
import faraday
class WebhookListener < Redmine::Hook::Listener
# issueが新規に作成されると呼ばれます。
def controller_issues_new_after_save(context = {})
issue = context[:issue]
controller = context[:controller]
return unless webhooks
post(issue)
end

# Jenkinsジョブにissue.idをパラメータ付きビルドで渡します。
def post
conn = Faraday.new(:url => 'http://jenkins') do |builder|
builder.request :url_encoded
builder.response :logger
builder.adapter :net_http
end
res = conn.post "/JOB_NAME/buildWithParameters", { issue_id: issue.id}
end
end

以下を参考にしました

まとめ

結果と所感について..。

  • 目論見どおり大変だったチケットの再振り分けが減りました
    • 実は私もヘルプデスクに担当を持っていますが、チケット対応が以前より楽になったと実感しています
  • 意外だったのは、epoch数が10回程度でも思ったよりと正答率が高い(約80%)ということ
  • Deep Learning の登場で機械学習の敷居は相当下がっていると感じます。みなさんも是非チャレンジしてみてください!

あいちゃん は今後も大きく育てていきます!

  • チケットの担当者振り分け
    • カテゴリ毎にだいたい同じような担当者になるので、一緒に入れてしまえるのでは?
  • あいちゃんと対話できるようにしたい
    • チャット形式でチケット起票における質問にある程度答えてくれると助かるのでは?

フューチャーアーキテクトでは、技術的視点だけでなく、ビジネス視点からも応用先を考え技術検証・現場への導入を行っています。
興味がある方、一緒に働きましょう! ぜひメッセージ下さい。

http://www.future.co.jp/recruit/