SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

NextremerのAI研究開発エンジニアが解説する三歩先のテクノロジー

人間と同程度の適応能力を持ったAI「汎用人工知能」とは?~General AI Challengeで汎用人工知能を作ろう

NextremerのAI研究開発エンジニアが解説する三歩先のテクノロジー 第1回

エージェントを作ってみよう(2)

エージェントを構成するクラス

 今回作るエージェントを構成するのは次のようなクラスです。

骨格をなすクラス

  • Incident:1ステップの環境とエージェントのやり取り事例
  • BaseModel:モデルのベースクラス
  • ControllerNet:コントローラの強化学習用のニューラルネットワーク
  • MyLearner:CommAI-envと接続するエージェント

具体的なモデル

  • RandomModel:環境入力とは関係なくランダムに出力するモデル
  • SpecialCharacterModel:環境入力とは関係なく特定コードを出力するモデル
  • LPCModel:最後に正の報酬を得たコードを出力するモデル
  • MapModel:環境入力ごとに正の報酬が得られる出力を総当たりで探索し、出力するモデル
  • RepeatModel:環境入力を繰り返すモデル
  • FeedbackModel:報酬0のときに、前回の環境入力と今回の環境入力を対応付けるモデル

 以下では骨格をなすクラスを中心に解説を行います。なお、行列の変形などコードの一部は省略しました。コメント付きのコードの全体がダウンロード可能ですので、興味のある方はご覧ください。

Incident

python
class Incident(object):
    '''毎回の環境とエージェントの相互作用事例'''
    def __init__(self, s=None, d=None, m=None, 
            c=None, t=None, r=None, ns=None, nd=None):
        self.s = s   # 環境入力
        self.d = d   # 各モデルの判断
        self.m = m   # 選択したモデル
        self.c = c   # 選択したモデルの出力コード
        self.t = t   # 出力コードは活用か探索か
        self.r = r   # 得られた報酬
        self.ns = ns # 次の環境入力
        self.nd = nd # 次の各モデルの判断

 今回は1つの事例(Incident)を環境入力(s)、各モデルの判断(d)、選択したモデルのid(m)、選択したモデルの出力コード(c)、出力コードのタイプ(t)、選択した行動をとった後得られた報酬(r)、次の環境入力(ns)、次の各モデルの判断(nd)の計8要素で表すものとしました。事例の用途は2つです。まず、モデルの内部状態を更新するため、nd以外が揃う度に最新の事例がモデルに渡されます。また、Q関数ニューラルネットワークの重み更新にも使用します。ns, ndで次の事例の情報を部分的に保存しているのはQ-learningを用いるためです。Q-learningの「s-a-r-s(状態-行動-報酬-次の状態)」にはこのモデルの「(s, d)-(m, c, t)-r-(ns, nd)」が対応します。

BaseModelと実際のモデル

 BaseModelは実際のモデルの基底クラスとなります。実際のモデルは最新のIncidentを受けて自分の状態を更新するupdate関数と環境入力を受け取って判断を出力するinfer関数の2つを定義します。モデルの具体例としてMapModelを見てみましょう。

python
class MapModel(BaseModel):
    '''環境入力に対応する出力を総当たりで探索し、出力するモデル'''
    def __init__(self, code_list):
        super(MapModel, self).__init__(code_list)
        self.get_code = {code_list[i]:i for i in range(len(code_list))}
        self.map_dict = {} # 判明した入力-出力の対応関係
        self.next_id = {}  # ある入力でまだ試していない候補
    
    def update(self, incident):
        # 事例の報酬が正のとき、正しい入力-出力の対応関係として記憶する.
        if incident.r > 0:
            self.map_dict[incident.s] = incident.c
        # 知っている対応関係なのに事例の報酬が負の場合はその対応関係を忘れる.
        if incident.r < 0 and self.map_dict.setdefault(
                incident.s, BaseModel.no_conclusion) == incident.c:
            self.map_dict[incident.s] = BaseModel.no_conclusion
        # 入力のまだ試していない候補から今回の出力コードを除く.
        if incident.c in self.get_code:
            n = self.next_id.setdefault(incident.s, self.code_list.copy())
            if self.get_code[incident.c] in n:
                n.remove(self.get_code[incident.c])
    
    def infer(self, s):
        # 入力-出力の対応関係をまだ知らなければ、結論を保留する
        d = self.map_dict.setdefault(s, BaseModel.no_conclusion)
        if d == BaseModel.no_conclusion:
            n = self.next_id.setdefault(s, self.code_list.copy())
            # 候補が空のときはすべての可能性を再び列挙.
            if len(n) == 0:
                n = self.next_id[s] = self.code_list.copy()
            return (n.pop(0), BaseModel.exploration)
        # 入力-出力の対応関係を知っていれば、それを活用行動として出力候補とする.
        return (d, BaseModel.exploitation)

 このモデルは環境入力に対応した出力が必要な場合(Task2等)を想定しています。判明した入出力の対応関係(map_dict)と、各入力に対してまだ試していない候補(next_id)を内部に保持しており、出力の知識がなくても効率的に探索すべき候補を提示します。

 雰囲気がつかめてきたでしょうか? モデル-コントローラ構造のエージェントは、特化した行動選択ルールを持ったモデルを多数用意しておき、それらが提示した行動の中からコントローラに最適なものを選ばせることでタスククリアを目指します。次は、エージェントの肝であるコントローラのニューラルネットワークを見てみましょう。

ControllerNet

 ControllerNetは基底クラスをchainer.ChainとしたQ関数近似用のニューラルネットワークです。

 __init__でこのニューラルネットワークで使用する部品(別のChain、層)を登録し、それらを用いてどのように計算するのかを__call__に書きます。

python
class ControllerNet(chainer.Chain):
    def __init__(self,
            model_count,      # モデルの総数
            s_code_count=256, # 文字の総数
            embed_dim=128,    # 文字の埋め込み次元
            hidden_dim=128,  # 隠れ層の次元
        ):
        # 使用する部品の登録
        super(ControllerNet, self).__init__(
                embed=L.EmbedID(s_code_count, embed_dim), # 文字埋め込み
                l1=L.Linear(embed_dim+model_count*(embed_dim+1), hidden_dim), # 全結合層1
                l2=L.Linear(hidden_dim, hidden_dim), # 全結合層2
                l3=L.Linear(hidden_dim, hidden_dim), # 全結合層3
                l4=L.Linear(hidden_dim, model_count), # 全結合層4
            )
    
    def __call__(self, s, d, t, train):
        # Q値の計算
        d = self.embed(d).reshape((len(d), -1))
        y = F.concat([self.embed(s), d, t], axis=1)
        y = self.l1(y)
        y = F.relu(self.l2(F.dropout(y, 0.1, train=train))) # 10%の確率でdropoutする
        y = F.relu(self.l3(F.dropout(y, 0.25, train=train)))
        y = self.l4(y) # モデルの総数だけQ値が並んだものが出てくる
        return y

 部品には可変な重みがあり、それを変えていくことがネットワークの最適化です。chainerで「計算」を行うと、計算結果と計算過程がセットになったVariableと呼ばれるオブジェクトができます。そのVariableを引数としてさらに計算を進め、損失関数(エラー)まで計算を行います。そのVariableの計算過程を逆にたどることで誤差逆伝搬を行い、損失関数が小さくなる重みのずらし方を決定します。このネットワークは文字埋め込みと4層の全結合層からなる単純なものです(__init__)。2層目と3層目では入力の前にdropoutを行い、出力後に活性化関数としてreluを入れています。

Q関数近似ニューラルネットワークの構造(右は最上位層部分を拡大)
Q関数近似ニューラルネットワークの構造(右は最上位層部分を拡大)

 dropout:学習時に確率的に成分を0にする正則化手法です。ニューラルネットワークの汎化性能を高めることが知られています。

 relu:負の値は0にする関数ですf(x)=max(0, x)。計算が簡単で質も高い活性化関数として知られており、ニューラルネットワークにしばしば用いられます。

MyLearner

 MyLearnerクラスはエージェントと環境の接点になります。CommAI-envの仕様に合わせてlearners.base.BaseLearnerを継承しnext、reward関数を定義します。主要な関数は__init__、next、reward、select_code、update_netの5つです。

 まず、__init__関数です。これまで説明してきたモデルとQ関数ニューラルネットワークの初期化はここで行います。Q関数ニューラルネットワークの重みは問題を解くと同時に更新していくので、ニューラルネットワークの最適化手法もここで設定しておきます。今回は比較的よく利用される確率的勾配降下法の一種であるAdamと、過学習を抑える重み減衰正則化を用いています。

python
def __init__(self):
    # 出力コードをpritableに限定する.
    action_code_list = [ord(c) for c in list(string.printable)]
    # モデルの初期化
    self.models = [
            RandomModel(action_code_list),
            # (中略)
        ]
    # Q関数のニューラルネットワークの初期化
    self.net = ControllerNet(len(self.models))
    
    # ニューラルネットワークの最適化を行う方法の設定
    self.optimizer = optimizers.Adam(alpha=0.001)
    self.optimizer.setup(self.net)
    self.optimizer.add_hook(chainer.optimizer.WeightDecay(rate=0.0001))
    
    self.replay_memory = deque([], 10000) # 事例保存用のキュー
    self.rl_max_minibatch = 64            # 一度のニューラルネットワーク更新に用いる事例数
    self.rl_gamma = 0.9                   # Q関数の割引率
    self.incident = None                  # 処理中の事例

 next関数では4つの処理を行った後、エージェントの出力を環境に返します。4つの処理とは、新しい環境入力を処理中Incidentに追加してモデルのupdateの呼び出し、エージェントの出力コードを選択(select_code)、事例の保存、Q関数ニューラルネットワークの更新(update_net)となります。更新が2つに分かれているのは、モデルのupdateでは他のモデルの判断は使いませんが、Q関数ニューラルネットワークの更新では全体のモデルの判断も必要だからです。reward関数では報酬を処理中Incidentに追加します。

 環境入力からエージェントの出力コードを選択するselect_code関数では各モデルのinferで判断を求め、ControllerNetの__call__で1つの出力を選びます。コードは次のようになります。

python
def select_code(self, s):
    xp_s = self.net.xp.asarray([s], dtype='int32')
    # 環境入力からモデルに判断させる
    d = [m.infer(s) for m in self.models]
    xp_d = self.net.xp.asarray([d], dtype='int32') # tupleのlistをnumpyのndarrayに変換
    xp_d, xp_t = xp_d[:, :, 0], xp_d[:, :, 1].astype('float32') # 出力コードとタイプに分解
    # 環境入力と判断結果をQ関数ニューラルネットワークに入力
    y = self.net(xp_s, xp_d, xp_t, train=False)
    # Q関数が最大のモデルのidを取得
    m = int(y.data[0].argmax())
    # 保留の場合はid=0のモデルのコードを使用
    c = d[m][0] if d[m][0] != BaseModel.no_conclusion else d[0][0] 
    return (d, m, c, d[m][1])

活用と探索(exploitation & exploration)の問題

 通常のQ-Learningでは常にQ関数が最大の行動を選択するということはあまりしません。一定の割合でランダムな行動をとる(epsilon-greedy)法などが用いられます。その理由は、常に合理的な行動をとると、まだやったことのない行動の価値がわからないままになる恐れがあるからです。このバランスは活用と探索の問題として知られています。

 今回はタスク遂行中に探索行動をとってしまうといつまでたっても次のタスクに進めない、タスクが切り替わると最適な行動が変わるので探索が進むという予想から、常にQ関数が最大の行動を選択することとしています。

 最後にupdate_netです。この関数は過去の事例に基づいてQ関数ニューラルネットワークの重みを更新します。前半は複数の事例のランダムサンプリング、後半は損失関数の計算と重みの更新です。損失関数の計算は囲みで説明したQ-Learningの重み更新法に対応しています。Deep Q-Networkにおいて、過去の事例を覚えておき複数事例から計算される更新量を用いる手法は、効率を高めるために広く用いられ、Replay Memoryとして知られています。

python
def update_net(self, replay_memory):
    xp = self.net.xp
    # replay_memoryから事例をサンプリング
    mb = replay_memory if len(replay_memory) <= self.rl_max_minibatch else \
           [replay_memory[i] for i in np.random.choice(
           range(len(replay_memory)), self.rl_max_minibatch)]
    mb_size, output_dim = len(mb), len(self.models)
    
    if mb_size > 0:
        s = xp.asarray([m.s for m in mb], dtype='int32')
        # (中略) d, t, m, r, nd, ntについても以下同様にリストをテンソルに変換
        
        # サンプルした事例の(次の環境入力、次のモデルの判断)から、
        # (未来の報酬の推定値 + この回の報酬)を教師信号とする
        future_r = self.net(ns, nd, nt, train=True).data.max(axis=1)
        t_signal = r + self.rl_gamma*future_r
        
        # サンプルした事例の(環境入力、モデルの判断)から、各コードのQ値を計算し
        # 実際に使われたコードのQ値だけを取り出し
        a_mask = xp.broadcast_to(xp.arange(0, output_dim)[None, :],
            (mb_size, output_dim))
        a_mask = (a_mask == m[:, None])
        q_selected = F.sum(self.net(s, d, t, train=True) * a_mask, axis=1)
        
        # 実際に使われたコードのQ値と教師信号の差の2乗を損失として計算
        loss = F.sum((q_selected - t_signal)**2)/mb_size
        
        # 誤差逆伝搬して勾配を計算し、重みを更新
        self.net.zerograds()
        loss.backward()
        self.optimizer.update()

エージェントの実行

 作成したスクリプトをsrc/learners内に移動します。仮にmy_leraner.pyと命名します。下記のコードを実行するとエージェントの評価が始まります。

python src/run.py src/tasks_config.challenge.json -l learners.my_learner.MyLearner

 筆者の環境では約30000時間ステップの後MicroTask5sub2までクリアすることができました。このタスク以降はマッピングを取るべき条件が複雑になるため、モデル間の相互作用の実装やハードコードしたモデルの出力を見て出力を決めるニューラルネットワークモデルが必要になると考えられます。

まとめ

 汎用人工知能は人と同じ程度の適応力をめざした人工知能です。現在の人工知能の技術は問題を限定しますが、汎用人工知能は様々な問題を解決する方法を学びます。知識を問題解決の道具と考える立場では、汎用人工知能の学習にはGradual LearningとGuided Learningが重要です。

 GoodAIが主催するGeneral AI Challengeは今後複数のラウンド開催が予定されている汎用人工知能開発コンペティションです。今年の2月に始まった第1ラウンドではFacebook AI ResearchCommAI-envを用いた文字列ベースの環境でエージェントのGradual Learningする能力を競います。

 具体的なエージェントのサンプルとしてモデル-コントローラ構造のエージェントを実装しました。今回の実装は作りこみした部分が大きく、筆者がタスクセットを見て実装したところまでしか進めませんが、モデルを学習可能なものに変えてみる等発展も考えられます。

参考文献

  • Y. LeCun, Y. Bengio, G. Hinton. Deep learning. Nature, 521(7553):436-444, 2015.
  • M. Rosa, J. Feyereisl and GoodAI Team. A FRAMEWORK FOR SEARCHING FOR GENERAL ARTIFICIAL INTELLIGENCE version 1, 2016.
  • M. Minsky著、安西祐一郎訳『心の社会』, 産業図書, 1990.
  • M. Baroni et al. COMMAI: EVALUATING THE FIRST STEPS TOWARDS A USEFUL GENERAL AI, ICLR, 2017.
  • M. Rosa, GoodAI. General AI Challenge Specifications of the First(Warm-Up)Round: Gradual Learning - Learning Like a Human version 2, 2017.
  • V. Mnih et al. Playing Atari with Deep Reinforcement Learning, NIPS, 2013.

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
この記事の著者

壹岐 太一(株式会社Nextremer)(イキ タイチ)

 株式会社Nextremerにて深層学習の論文調査や新規アルゴリズムの開発を行っています。機械学習の論文読み会を定期開催しています。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/10199 2017/08/07 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー