SIGMA-SE Tech Blog

SIGMA-SE Tech Blog


当サイトは、過去に運営していた別ドメイン(unisia-se.com)から sigma-se.com へ移行した技術ブログです。
旧サイトの記事をもとに、内容の精査・加筆・最新化を行い再構成しています。
正確で実用的な情報提供を目的としています。

Python - タスク指向型対話:4/5 フレームベースの環境準備(SVM・学習データ)

概要

フレームベースのタスク指向型対話システムに向けて、SVM(scikit-learn)の役割と学習データの作成手順を整理する。

フレームベースでは、発話から対話行為タイプや必要なスロット情報を推定し、対話管理に渡す。状態遷移ベースより柔軟な入力を扱いやすくなる一方で、学習データの作り方が重要になる。

ここでは、SVMの準備、発話行為タイプとコンセプトの定義、学習データ生成の流れを確認する。

この記事で扱うこと

  • フレームベースの対話システムの考え方。
  • 対話行為タイプ、コンセプト、スロットの関係。
  • SVMを使って発話を分類する理由。
  • 学習データを作成する流れ。

作業前に確認すること

確認項目 内容
前提記事状態遷移ベースの構成とTelegram連携を確認しておく。
Python環境scikit-learnとdillをインストールできる状態にする。
学習データ設計発話行為タイプとコンセプトの候補を先に決めておく。

作業時の注意点

作業時の注意点 確認する観点
フレームと状態の違い状態遷移は流れを管理し、フレームは発話から埋める情報構造として考える。
学習データの偏り例文が少ない、表現が偏ると、推定結果も偏りやすい。
用語の違いインテント、エンティティ、スロットなどはサービスによって呼び方が異なる。

実装内容

タスク指向型対話システム(フレームベース)のシステム構成について

システム構成は、状態遷移ベースと同様にPythonで作成する対話システムTelegramサーバーTelegramをインストールしたクライアント端末(Android、iPhone、Windows、Linux、Macなど)の単純な3構成でデータベースも使わない。
※ 以降、Telegramインストールした端末をTelegramクライアントと表現する。

大まかな処理の流れとしては、Pythonで作成する対話システムを起動すると、これがBotとしてTelegramクライアントに表示され、ユーザー要求の入力待ち状態となる。

その後、ユーザーが要求(入力)するとTelegramサーバーを介し、対話システム(Bot)の応答制御処理を経て、結果をユーザーに返し対話していく。

ここからは、SVM(sklearn)の概要、インストール、学習データ作成を確認する。

また、次の記事以降も継続して解説していく、タスク指向型システムのフレームベースについての動作環境は、状態遷移ベースと同じでTelegramクライアントがWindows、Pythonの対話システムがLinux(CentOS7)となるので、コマンドは特に自身の環境に合わせて読み替えること。

フレームと対話行為を推定するSVM(sklearn)の概要とインストール

まず、フレームとは、一度に複数の情報を受付けられるようにスロットと呼ばれる属性からなる入力情報を複数持つデータ構造のことで、遂行手法としてフレームベースとも呼ばれる。

フレームベースの基本処理構成は、発話処理部対話管理部(状態更新、行動決定)、発話生成部から成っていて、パイプラインアーキテクチャまたはVモデルとも呼ばれる。
※ ここでは、フレームベースに関する解説は概要レベルに留め、次の記事以降でSVMやCRFを絡めた実装を例に掘り下げていく。

始めのユーザーの発話直後に処理される発話処理部では、発話内容から発話の意図となる対話行為を推定する。

対話行為は、対話の大分類(意図)を表す対話行為タイプとその詳細情報であるコンセプトで構成される。

例えば、ユーザーの入力値が「福岡の明日の天気は?」の場合 発話行為タイプを「天気情報の要求」に分類し、コンセプトの場所(都道府県)を「福岡」、日付を「明日」、情報種別を「天気」という具合に分類(推定)する。

対話行為タイプとコンセプトは、開発環境(*1)、(*2)によって呼び方が違うので注意。

  • (*1)「Google Home」は、対話行為タイプをインテント、コンセプトをエンティティと呼ぶ。
  • (*2)「Amazon Alexa」は、対話行為タイプをインテント、コンセプトをスロットと呼ぶ。
    (上記フレームのスロット(属性/値)と(*2)のスロットは表現は同じ「スロット」という表現だが意味が違うので混同注意。)

この対話行為タイプを推定するためにパターン識別用の機械学習方法の一つであるSVM(support vector machine)を使用する。
※ パターン識別の理屈については、ここで触れないがデータを2つのグループに分類する問題に優れており、2分類化することにおいては、最も優秀な識別能力を持つとされる。

SVMによる学習は、Pythonの機械学習ライブラリであるsklearn(scikit-learn)を用いて実装する。

  • sklearn, dillのインストール

    $ pip3 install sklearn
    

    また、学習結果を保存するため シリアライズモジュールであるdillもインストールする。

    $ pip3 install dill
    

以上がSVMの環境準備。

また、対話行為の分類基準を確立するため、学習データ(事例)が必要になってくるため、次項で作成方法を解説する。

学習データの作成

この案内対話で使用する発話行為タイプとコンセプトを以下のように定義する。

  • 発話行為タイプ

    発話行為名 発話行為キー 備考
    天気情報の要求 request-weather ユーザーが天気情報を要求している。
    伝達情報の初期化 initialize ユーザーが案内の初期化を要求している。
    伝達情報の訂正 correct-info ユーザーが発話行為の推定を訂正要求している。
  • コンセプト

    属性名 属性キー
    場所(都道府県) place 都道府県のいずれか
    日付 date 今日、明日等の日付
    情報種別 type 天気 or 気温

例えば、上記発話行為タイプの学習データを作成する場合
前方を「発話行為キー」、後方を「ユーザーの入力値」とし、そのまま愚直に書くと

request-weather 福岡の天気は?
request-weather 明日の福岡の気温教えて
correct-info 大阪じゃなくて福岡です
correct-info 天気じゃなく気温
initialize もう一度はじめから
initialize リセットして!
…

という具合に発話行為タイプ別の都道府県別でさらにニュアンスを変えた学習データを一つ一つ手動で書かなければならず、現実的でないため、タグ付けした学習データを作り、これを量産するプログラムを準備する。

まず、学習データにコンセプトの属性キーでタグを付けた代表データ(examples.txt)なるものを準備する。
※ da=XXXX でどの発話行為タイプのデータか定義している。

  • examples.txt
    da=request-weather
    大阪
    大阪です
    明日
    明日です
    天気
    天気です
    大阪の明日
    大阪の明日です
    大阪の天気
    大阪の天気です
    大阪の天気を教えてください
    明日の天気
    明日の天気です
    明日の天気を教えてください
    明日の大阪の天気
    明日の大阪の天気です
    明日の大阪の天気を教えてください
    大阪の明日の天気
    大阪の明日の天気です
    大阪の明日の天気を教えてください
    
    da=initialize
    もう一度はじめから
    はじめから
    はじめからお願いします
    最初から
    最初からお願いします
    初期化してください
    キャンセル
    すべてキャンセル
    
    da=correct-info
    大阪じゃない
    大阪じゃなくて
    大阪じゃないです
    大阪ではありません
    明日じゃない
    明日じゃなくて
    明日じゃないです
    明日ではありません
    天気じゃない
    天気じゃなくて
    天気じゃないです
    天気ではありません
    

次に上記代表データを読込んで対象のタグに応じた辞書(都道府県名、日付、情報種別)からランダムで一つ抽出し、学習データを作成するプログラム(generate_da_samples.py)を準備する。
※ 下記のプログラムでは、代表データ一行あたり\(1000\)個の学習データを作成している。

  • generate_da_samples.py
    import re
    import random
    import json
    import xml.etree.ElementTree
    
    # 都道府県名のリスト
    prefs = ['三重', '京都', '佐賀', '兵庫', '北海道', '千葉', '和歌山', '埼玉', '大分',
            '大阪', '奈良', '宮城', '宮崎', '富山', '山口', '山形', '山梨', '岐阜', '岡山',
            '岩手', '島根', '広島', '徳島', '愛媛', '愛知', '新潟', '東京',
            '栃木', '沖縄', '滋賀', '熊本', '石川', '神奈川', '福井', '福岡', '福島', '秋田',
            '群馬', '茨城', '長崎', '長野', '青森', '静岡', '香川', '高知', '鳥取', '鹿児島']
    
    # 日付のリスト
    dates = ["今日","明日"]
    
    # 情報種別のリスト
    types = ["天気","気温"]
    
    # サンプル文に含まれる単語を置き換えることで学習用事例を作成
    def random_generate(root):
        buf = ""
        # タグがない文章の場合は置き換えしないでそのまま返す
        if len(root) == 0:
            return root.text
        # タグで囲まれた箇所を同じ種類の単語で置き換える
        for elem in root:
            if elem.tag == "place":
                pref = random.choice(prefs)
                buf += pref
            elif elem.tag == "date":
                date = random.choice(dates)
                buf += date
            elif elem.tag == "type":
                _type =  random.choice(types)
                buf += _type
            if elem.tail is not None:
                buf += elem.tail
        return buf
    
    # 学習用ファイルの書き出し先
    fp = open("da_samples.dat","w")
    
    da = ''
    # examples.txt ファイルの読み込み
    for line in open("examples.txt","r"):
        line = line.rstrip()
        # da= から始まる行から対話行為タイプを取得
        if re.search(r'^da=',line):
            da = line.replace('da=','')
        # 空行は無視
        elif line == "":
            pass
        else:
            # タグの部分を取得するため,周囲にダミーのタグをつけて解析
            root = xml.etree.ElementTree.fromstring("<dummy>"+line+"</dummy>")
            # 各サンプル文を1000倍に増やす
            for i in range(1000):
                sample = random_generate(root)
                # 対話行為タイプ,発話文,タグとその文字位置を学習用ファイルに書き出す
                fp.write(da + "\t" + sample + "\n")
    
    fp.close()
    

以下、上記generate_da_samples.py処理の実行順解説。

  • 都道府県名、日付、情報種別の辞書を定義

    … (省略) …
    # 都道府県名のリスト
    prefs = ['三重', '京都', '佐賀', '兵庫', '北海道', '千葉', '和歌山', '埼玉', '大分',
            '大阪', '奈良', '宮城', '宮崎', '富山', '山口', '山形', '山梨', '岐阜', '岡山',
            '岩手', '島根', '広島', '徳島', '愛媛', '愛知', '新潟', '東京',
            '栃木', '沖縄', '滋賀', '熊本', '石川', '神奈川', '福井', '福岡', '福島', '秋田',
            '群馬', '茨城', '長崎', '長野', '青森', '静岡', '香川', '高知', '鳥取', '鹿児島']
    
    # 日付のリスト
    dates = ["今日","明日"]
    
    # 情報種別のリスト
    types = ["天気","気温"]
    … (省略) …
    
  • da_samples.datを書き込みモードで定義
    作成する学習用データファイルda_samples.datを新規作成し、書き込みモードで開いたfpを定義する。
    ※ openは、指定したファイルが存在しなければ新規作成した状態で開き、存在していれば上書き状態で開く。
    但し、指定したファイルがおかれているディレクトリが存在しない場合は、エラーとなるので注意。

    … (省略) …
    # 学習用ファイルの書き出し先
    fp = open("da_samples.dat","w")
    … (省略) …
    
  • examples.txtファイルの読み込み
    上記で準備した代表データ(examples.txt)を読み込みモードで開き、読み込んだ行数分ループし(*3)or(*4)or(*5)を実行する。

    • (*3)正規表現モジュールreda=から始まるかどうかチェックし、始まるのであれば、右辺の発話行為タイプをdaに保持する。
    • (*4)空白行とみなしスキップし、何もしない。
    • (*5)XMLとして解析するため(最上位のタグは1つでなければならない)、対象行をdummyタグで囲い、fromstringで文字列からxml.etree.ElementTree.Elementにパースしたものをrootに保持する。 このrootを引数に\(1000\)回random_generateメソッド(処理詳細は下記(*6)を参照)を呼出し、対話行為タイプとrandom_generateの戻り値samplefpに書き込む。
      da = ''
      for line in open("examples.txt","r"):
          line = line.rstrip()
          if re.search(r'^da=',line):
              da = line.replace('da=','')
          elif line == "":
              pass
          else:
              root = xml.etree.ElementTree.fromstring("<dummy>"+line+"</dummy>")
              for i in range(1000):
                  sample = random_generate(root)
                  fp.write(da + "\t" + sample + "\n")
      
    • (*6)random_generate メソッドの処理 引数のroot内にタグが存在しない場合(len(root) == 0)、root直下のテキストをそのまま返す。
      以降は、root内のタグに応じて、都道府県名 or 日付 or 情報種別の辞書からそれぞれからランダム抽出した結果と対象タグ末尾の発話文(tail)をbufに加算して返す。
      def random_generate(root):
          buf = ""
          if len(root) == 0:
              return root.text
          for elem in root:
              if elem.tag == "place":
                  pref = random.choice(prefs)
                  buf += pref
              elif elem.tag == "date":
                  date = random.choice(dates)
                  buf += date
              elif elem.tag == "type":
                  _type =  random.choice(types)
                  buf += _type
              if elem.tail is not None:
                  buf += elem.tail
          return buf
      
  • 最後にfpを閉じて処理終了

    … (省略) …
    fp.close()
    … (省略) …
    

最後に実行確認

  • generate_da_samples.pyの実行確認
    上記の代表データ(examples.txt)がgenerate_da_samples.pyと同じフォルダ内にある状態で実行する。
    (もちろんgenerate_da_samples.pyまでのファイルパスは各自環境に合わせて読み替える。)

    $ python ~/gitlocalrep/dsbook/generate_da_samples.py
    
  • da_samples.datの確認
    代表データ(examples.txt)の学習データが40個でそれぞれ\(1000\)個の学習データを出力しているため、da_samples.datは、\(10000\)個のデータとなっている。

    $ cat ~/gitlocalrep/dsbook/da_samples.dat
    request-weather 高知
    request-weather 大分
    request-weather 京都
    … (省略) …
    equest-weather 青森です
    request-weather 神奈川です
    request-weather 神奈川です
    … (省略) …
    request-weather 今日
    request-weather 明日
    request-weather 今日
    … (省略) …
    initialize もう一度はじめから
    initialize はじめから
    initialize はじめからお願いします
    … (省略) …
    correct-info 福岡じゃない
    correct-info 神奈川じゃない
    correct-info 広島じゃない
    … (省略) …
    

以上で学習データの準備が完了。

実務とのつながり

  • 自然な入力への対応
    ユーザーが一文で複数情報を伝える場合、フレームベースの方が扱いやすい。
  • 機械学習との接続
    発話分類をSVMで扱うことで、ルールだけでは拾いにくい表現にも対応しやすくなる。

まとめ

  • フレームベースでは、発話から対話行為タイプやスロット情報を推定する。
  • SVMは発話行為タイプの分類に使う。
  • 学習データの設計と品質が、後続の推定精度に大きく影響する。

参考文献

  • 東中 竜一郎、稲葉 通将、水上 雅博(\(2020\))『Pythonでつくる対話システム』株式会社オーム社

GitHubサポートページ



Copyright SIGMA-SE All Rights Reserved.
s-hama@sigma-se.jp