Gakushukun1’s diary

20代エンジニア, 統計的機械学習勉強中 twitter: @a96665004

車の評価の推測に用いた学習済みネットワークの構造の分析

目的

階層型ニューラルネットの学習結果を表示して, その構造の考察を行う。


gakushukun1.hatenablog.com

前回の記事で車の評価の推測を行った学習済みネットワークについて, 具体的な構造を図で確認し, 分析してみた.

前回の記事で示したネットワークの大まかな構造は次のようになっている.
f:id:gakushukun1:20190507232430p:plain

前回の実験では, このネットワークに対して, L1正則化のハイパーパラメータ \lambda = 1.0 \times 10^{-5}を設定し, ユニット間の結合がスパースになるよう学習した.
今回は, その学習済みネットワークの各ユニット間の結合の重みを図示することで, 入力・出力間にどのような関係があるかを探ってみる.

具体的に学習済みネットワークを図示するための関数は次の通り. なお, 動作にはpydotおよびGraphvizが必要である.

def draw_graph(model, filename, weight=True, threshold=0, input_label=[], output_label=[]):

    input_shape = model.get_input_shape_at(0)[1]
    output_shape = 0

    graph = pydot.Dot(graph_type='digraph', rankdir='LR', splines='false', dpi='200', ranksep='1.5')
    graph.set_node_defaults(label='', shape='circle', width='0.3', height='0.3', fontsize='7', penwidth='0.3', margin='0')
    graph.set_edge_defaults(arrowsize='0.3', penwidth='0.3')
    idx = 0

    if len(input_label) == input_shape:
        input_label = []

        for i in range(input_shape):
            input_label.append(str(i))

    for i in range(input_shape):
        graph.add_node(pydot.Node('i.' + str(i), label=str(i)))
        graph.add_edge(pydot.Edge('i.' + str(i), str(idx) + '.' + str(i)))

    for layer in model.layers:
        if type(layer) != Dense:
            continue

        weights = layer.get_weights()[0]

        wx, wy = weights.shape

        max_w = np.max(weights.flatten())
        min_w = np.min(weights.flatten())

        for i in range(wx):
            for j in range(wy):
                edge = pydot.Edge(str(idx) + '.' + str(i), str(idx + 1) + '.' + str(j))
                if weight:
                    if weights[i][j] > threshold:
                        edge.set_penwidth(weights[i][j] / max_w)
                        edge.set_color('red')
                        graph.add_edge(edge)
                    elif weights[i][j] < -threshold:
                        edge.set_penwidth(weights[i][j] / min_w)
                        edge.set_color('blue')
                        graph.add_edge(edge)
                    else:
                        edge.set_style('invis')
                        graph.add_edge(edge)
                else:
                    graph.add_edge(edge)

        idx += 1
        output_shape = wy

    if len(output_label) == output_shape:
        output_label = []

        for i in range(output_shape):
            output_label.append(str(i))

    for i in range(output_shape):
        graph.add_node(pydot.Node('o.' + str(i), label=str(i)))
        graph.add_edge(pydot.Edge(str(idx) + '.' + str(i), 'o.' + str(i)))

    graph.write_png(filename)

この関数に対して, 学習済みのKerasのモデルを次のように渡す.

draw_nn.draw_graph(model, 'car_model.png', threshold=0.5)

ここで, thresholdは重みの絶対値がそれ以下の結合を図に表示させないための閾値である. 結果として得られる出力は次にようになる.
f:id:gakushukun1:20190513230023p:plain

与えられた閾値より, 図では絶対値0.5以下の重みの結合は省略されている. また, 重みが正ならば赤, 負ならば青で表示し, 重みの絶対値が大きい結合ほど太く表示するようにしている.
図中で, 0, 1, 2, 3, 4, 5で表されている入力は, それぞれ

  • 0 ... buying(価格) 数値が大きいほど高い
  • 1 ... maint(管理費) 数値が大きいほど高い
  • 2 ... doors(ドアの数) 数値が大きいほど多い
  • 3 ... persons(乗員数) 数値が大きいほど多い
  • 4 ... lug_boot(載せられる荷物の量) 数値が大きいほど多い
  • 5 ... safety(安全性) 数値が大きいほど高い

を表している.

一方, 0, 1, 2, 3で表されている出力は, 4つある評価ラベルそれぞれに所属する確率を表す. 評価ラベルの詳細は次の通り.

  • 0 ... 良くない
  • 1 ... まあまあ
  • 2 ... 良い
  • 3 ... とても良い

図を見ると, 入力3の「荷物の載せられる量」が, (正の結合-正の結合-正の結合)をたどるの2つのルートで出力3につながっていることが見て取れる. このことから, 「荷物の載せられる量」が車の高い評価に正の影響を与えていることが分かる. また, 入力0の「価格」が, (負の結合-正の結合-正の結合)をたどるルートで出力3に, (負の結合-負の結合-負の結合)をたどるルートで出力1,2につながっていることも見て取れる. このことからは, 「価格」の高さが上位3個の評価(まあまあ, 良い, とても良い)に負の影響を与えていることが分かる. さらに, 入力2の「ドアの数」はあまりどの出力にも影響を与えていないことも分かる.

まとめ

階層型ニューラルネットの学習結果を図示し, 結合の正負・重みを確認することで, 入力から出力へどのような推論が行われているかの分析を行った。