word2vec の結果を利用して RNN で文章を生成してみる(2)
word2vec の出力結果を元に文章を作ってみるコーナーの第二弾です。 今回はエンコーダー・デコーダーモデルを使用して word2vec の出力から文章を生成できないかを試してみました。 使用したモデルは以前の記事で紹介した Skip-Thought Vectors です。
Skip-Thought Vectors を簡単に説明すると、入力文をエンコーダーでエンコードしその文の情報をコンテキスト情報として指定サイズのベクトルに圧縮、そしてこのコンテキスト情報を元に、デコーダーを使用して入力文の前後の文を出力しようというモデルとなります。 この Skip-Thought Vectors の入力および出力は、単語のIDを各ユニットに割り当てた形となります。今回は、この入力と出力を word2vec にして日本語で動かしてみました。
chainer を用いて、前回も利用した mecab で分かち書きにした wikipedia の記事文章とその文章から生成した word2vec を利用して処理を行いました。ロス関数は本家のソフトマックスのままでは利用できないので、正解単語とデコーダーによって出力された単語との二乗誤差を用いました。対象となった単語は40万語ほどで、それ以外は未知語としてすべて同じベクトルを割り当てるようにしています。
利用したコードは記事の最下部に記載しています。一部 util.py に逃がしたコードは記載していませんが、だいたい何をやっているかは分かっていただけるかと思います。chainer にはコンディショナルな GRU を扱う方法がないので、StatefulGRU のコードを利用して ConditionalStatefulGRU を自作、それをさらに自作の Skip-Thoughts モデルで使用しています。Skip-Thoghts モデルでは、まず入力単語をエンコーダー部でエンコードし、その出力をデコーダーで使用して前後の文が出力されるように学習しています。
だいたい6万文ほど学習させた段階で誤差が小さくならなくなってきたので、今回はそこで処理を停止させました。以下はその時のモデルを利用して、文章を入力した際に、次の文章として生成されたものです。文章の生成は、「。」が現れるか、30単語生成した時点で停止するようにしています。ちなみに入力文章は、学習したデータの中にも含まれている、wikipedia のアンパサンドに関する説明の一節です。
- 入力文:アンパサンド(&)とは「…と…」を意味する記号である。
出力単語列:'000', '受け取る', '回数', '推定', '平均', '000', '賃金', '月末', '政令', '条', '詐欺', '弁護士', '監査', '地裁', '年金', '協定', '加盟', '審議', '令', '奉仕', '自治体', '在日', '捕鯨', '定める', '通知', '土木', '請求', '職員', '実務', '事務'
入力文:その使用は1世紀に遡ることができ(1)、5世紀中葉(2,3)から現代(4-6)に至るまでの変遷がわかる。
- 出力単語列:'フラッシュ', 'メニュー', 'フォーマット', '送信', 'ケーブル', '媒体', 'ホスト', '動画', 'MHz', '閲覧', 'エージェント', '互換', '通信', '準拠', 'タイミング', 'サポート', 'OS', 'メニュー', 'SD', 'ボード', 'ファイル', '周波数', 'ゲート', '本体', 'マイクロソフト', 'シフト', 'コンテンツ', 'オブジェクト', 'Microsoft', 'ロード', 'フォーマット', 'ドメイン', '選択肢', 'オブジェクト', 'ソニー', 'ウェア', 'マイクロソフト', 'データ', '素子', 'オート', 'アプリ', 'コア', '動画', 'スケジュール', 'グループ', '夏季', 'メイン', '祭り', 'ライフ', 'ロケ', 'ショップ'
ご覧いただいた通り、全然うまく学習できていません。入力文の話の流れを完全に無視しているだけでなく、出力がただの名詞の羅列でありそもそも文章として成り立たせるための学習が行われたように全く見えません。一方、出力する単語列はその前の単語に大きく影響されるところがあり、連想ゲームのように関連する単語が生成されるようになってしまっています。
「てにおは」が全くなく、その前の単語に近い単語が生成されるというのは、学習動作から考えて一応想定していた結果ではあります。ある文章には、それ以前に入力されてきた単語に関連する単語(word2vec 的に距離が近い単語)が現れる確率が、それ以外の単語が現れる確率より格段に高いので、入力された単語と近い単語を出力するような学習となったのでしょう。
入力文のコンテキストを完全に無視するという挙動はなんとも言えません。同じ文の組み合わせを一回ずつしか学習させていないので十分に学習しきれていない可能性もありますし、自前で実装した ConditionalStatefulGRU がうまく機能していない可能性もあります。一応 ConditionalStatefulGRU のパラメーター更新は行われているようですので、そんなことはないと思いたいものですが。
とりま、前者の挙動をなんとかしないと、文章っぽいものを生成することすらできません。この辺りをどう解決するかが今後の鍵になりそうです。
Deep Learning で使われてる attention ってやつを調べてみた
先週 Skip-Thought Vectors について調べてみたわけですが、その中でよく何を言っているのかよく分かっていなかった、 attention mechanism について調べてみました。 調べるにあたって、最近のDeep Learning (NLP) 界隈におけるAttention事情 が大変参考になりました。ありがとうございます。
まず attention 、特にエンコーダー・デコーダーモデルにおける attention について簡単に説明すると、入力情報全体ではなく、その一部のみを特にフォーカスしたベクトルをデコーダーで使用する仕組みのことです。そのことにより、デコードの特定のタイミングにだけ必要になる入力情報を精度よく出力に反映させることができるようになります。
これだけでは何のことかちょっと分かりにくいので、Neural machine translation by jointly learning to align and translate を例に説明していきたいと思います。この論文は、Neural Machine Translation という Neural Network を利用して翻訳タスクを行う分野の論文ですが、最近その分野でも著しい成果を出しているエンコーダ・デコーダーモデルでの翻訳処理に、attention を導入することによって、翻訳前の文章が長文であっても精度よく翻訳処理が行えるようにしたという成果を紹介しています。
通常のエンコーダ・デコーダモデルでは、エンコーダの出力は一つしかデコーダでは使用されません。それをどのように用いるかは手法によりけりですが、この方法では、入力文の情報を特定のサイズのベクトルにまとめる必要があり、長文になればなるほど元の情報の圧縮精度が悪くなってしまいます。
一方、attention を用いたモデルでは、エンコーダーの隠れ層のうち、特定の入力単語やその周辺の単語にフォーカスしたベクトルをデコーダで用います。これにより、デコーダのある時点で必要な情報にフォーカスして使用することができ、入力文の長さに関係なくデコードを効率よく行うことができます。attention の利用方法も手法によりけりですが、すべてのベクトルを重み付けして利用する global attention や特定のベクトルのみを用いる local attention と呼ばれる方法に分けている提案もあります。
上記の論文では、長文での翻訳精度が上昇したでけでなく、alignment と呼ばれる機械翻訳分野で、翻訳前の文と翻訳語の文の対照関係を分析する処理でも有用な結果を生成することができているようです。
個人的には、エンコーダで生成するベクトルに十分な長さがあれば特に問題なく似たような状況を学習できそうな気もするのですが、なかなかそれではうまくいったりしないものなんでしょうか。
参考文献
Skip-Thought Vectors を解説してみる
本日は、インスピレーションと予算の枯渇のため、実験ができていなかったので、論文の解説をいたします。まあ、解説とか偉そうなことを言っていますが、主に自分用のメモみたいなものなのですが。
紹介する論文は、「Skip-Thought Vectors」です。この手法は、文(センテンス)をベクトル化する手法の一つで、様々なNLPタスクで好成績を挙げたことで知られている去年の6月にarxivに公開された論文です。ちなみに著者の方が Theano 上で動くソースコードを公開しているので、実際に動かしてみることも可能です。(ただし、学習に時間がかかる) github.com
さて、この Skip-Thought Vectorsのですが、最大の特徴は教師なし学習でかなり質の高い文ベクトルを生成できる点にあります。実際に使用する入力データは、文章のコーパス(論文中ではブックコーパス)だけでできてしまいます。計算資源さえあればOKという、日曜大工な人間には最高に相性の良い手法となっています。
その構造ですが、下記の図が一番わかりやすいでしょう。入力しているのは、文書中のi番目の文。出力は、同じくi-1番目とi+1番目の文である入力文書の前後の文です。正確に言うと、入力文を元に文ベクトルを生成するエンコーダと、文ベクトルを元に前の文と次の文を生成する2つのデコーダから成っているモデルです。
論文中では、エンコーダに用いたモデルはGRU、デコーダに用いたモデルは conditional GRUを使用していますが、エンコーダ・デコーダとして使用できればモデル上なんでもいいことになります。
では実際にどのように学習していくのか。手順は下記になります。
- エンコーダに入力となる文を最後まで入力する(入力に用いる単語ベクトルはあらかじめ用意しておく。ID形式とかなんでもOK)。
- エンコーダの学習により獲得した隠れ層の値を入力文のベクトルとして使用する。
- デコーダに入力文の引数と、生成する文の一つ前の単語を入力する(最初の単語の場合は、eos 記号を入力とする)。
- デコーダのこの時の隠れ層の値と、出力される単語に対応する語彙ベクトルの内積を求め、この値をその時点での該当単語の出力される確率として扱う。
- 出力文の最後まで 3−4 の操作を繰り返し、単語生成確率の和を求め、この値が最大化されるようにエンコーダとデコーダのパラメータを学習する
デコーダは、前の文を生成するものと次の文を生成するものの2つがあり、基本的にそれぞれで使用するパラメータは違うものを使用しますが、語彙ベクトルだけは共通のものを使用します。というわけで、以上で学習は完了です。簡単ですね!
注意点としては、学習時に出現していない単語がテストセットに含まれている時の扱いです。論文中では、テストセットと学習セットに含まれる単語をword2vec等でベクトル化した後、学習セットに含まれる単語のベクトルを word2vec ベクトルから学習時に使用したベクトルに射影する線形変換を学習しすることにより、未知語の表現を獲得しエンコーダでも使用できるようにしています。 デコード時に出力される語彙に関する言及は特にないので、増やすことについての考慮は特にされていないと思われます。難しそうですものね。
また、学習時に使用する文ですが、 attention mechanism (参照:Neural machine translation by jointly learning to align and translate.) を使用して、適切な単語のみを考慮して学習できるように一部改変しながらもちているようです。
というわけで、後はエンコードしたりデコードしたりして遊んでみてねーとのことですね。エンコーダ・デコーダに使用するモデルを変更してみても面白いかもしてません。
2016.04.30 追記
attention mechanism については、エンコーダー・デコーダーモデルにそういう+αもあるよという一般的な説明であって、実際に論文で説明している手法には使用されていないようです。
word2vec の結果を利用して RNN で文章を生成してみる(1)
chainer のサンプルの中には RNN 利用して文章を学習し、コンテキストに沿った単語を選択できるようになる ptb のサンプルが付属しています。
今回はこいつをちょっと改造して、単語の識別IDではなく、word2vec で生成したベクトルを用いて ptb サンプルと同じことをやってみようと思いま......したが、残念がら chainer の仕様理解ができていなかったようで、一切パラメーター更新ができておらず、4000円ほどドブに捨てる結果となってしまいました。辛すぎる!
そういうわけで今日のところは、こういう風にやったらうまく学習できなかったという記録のみ記載しておきたいと思います。原因分かり次第、追記か別記事を書きます。
今回学習がうまくいかなかったモデルは以下のように実装しました。元の ptb サンプルから embedID のレイヤの関数を取り除き、Classification で loss を算出する代わりに、huber 損失関数という二乗誤差の親戚みたいな関数で loss を求めると言った構成になっています。入力は単語ベクトルで出力も単語ベクトルになります。LSTMかましていることからも分かるように、model をリセットするまでは以前の入力値も反映した出力をしてくれるモデルです。
class RNNLM(chainer.Chain): def __init__(self, n_vocab, n_units, train=True): super(RNNLM, self).__init__( l1=L.LSTM(n_vocab, n_units), l2=L.LSTM(n_units, n_units), l3=L.Linear(n_units, n_vocab), ) self.train = train def reset_state(self): self.l1.reset_state() self.l2.reset_state() def __call__(self, x, t): h1 = self.l1(F.dropout(x, train=self.train)) h2 = self.l2(F.dropout(h1, train=self.train)) y = self.l3(F.dropout(h2, train=self.train)) return F.huber_loss(y, t)
そして、このモデルの loss に対して特に何も考えずにもとの ptb サンプルと同様に、backword して、unchain して、optimizer を update したのですが、全くろくな結果を得ることができませんでした。調べてみると、重み値の更新が一切行われておらず、初期値のまま。 ......なんでや。Function でなくちゃんとレイヤをかまさなければならないのか、それとも loss の扱い方が悪かったのか。
<<追記>> 2016.4.17 夜
huber_loss で学習ができない件について、自己解決しました。huber_loss はミニバッチの学習には対応していないようで、そのために全く学習ができていなかったようです。実装上の都合なのか、関数の制限なのかは勉強不足ため理解できていませんが、ひとまず huber_loss の部分も二乗誤差に差し替えることにより無事学習が行えました。やったね!
しかし学習はできているようであるものの、全くいい感じに文章を覚えてくれません。やたらめったらΣを押してくるこになってしまいました。バッチサイズがでかすぎたのか、エポック数が足りないのか、単純に二乗誤差では学習がうまくいきにくいのか。ちょっと文献をあさってみる必要がありそうです。
Wikipedia を word2vec する
前回、青空文庫で word2vec を試してみましたが、結果を見ての通り、作家によって類似する単語が違ったり、そもそも語彙が少ないため、あまり汎用性のある結果を得ることはできませんでした。
そこで今回は、日本語 Wikipedia のダンプデータを使用して、word2vec で学習させてみました。 Wikipedia ではこちらに記載されているように、Wikipedia 上で作成された様々なデータのダンプデータを配布しています。主なものだと、全ページの要約や全文、変わったものだと、ページ間のリンク関係のデータなどが含まれています。 今回は、日本語の最新情報から全文情報を取得して、使用しました。ちなみに、このデータは圧縮時でも2GB、展開すると10GB近くあるデータになります。このデータは XML と wiki 記法で記述されておりそのままでは使用できないため、wikiextractorを使用して平文になおした後、mecab で分かち書きに変換して学習に用いました。ちなみに学習に用いたデータは3GBほどになります。
結果の比較のため、イテレーション回数と学習アルゴリズムを変えて試してみました。イテレーション回数は、1回と10回。アルゴリズムは、cbow と skip−gram を使用しました。word2vec と cbow の詳細は、前回の記事ををご覧ください。skip−gram は、cbow とは逆で単語を入力し文脈語を出力するニューラルネットワークを学習する点が特徴です。サンプリングなどの学習に用いるテクニックは cbow と共通ですが、バックプロバケーションの必要がある出力が多いため、cbow より3〜4倍ほど学習に時間がかかります。 CPU4コア、メモリ16GHzの環境だと、skip−gramでイテレーション回数を10回にした場合、だいたい丸二日ほどかかりました。
できれば評価データを用いて定量的に見たかったのですが、日本語での評価データを探しきれなかったため、前回と同様、何個かピックアップして結果を見ていきたいと思います。結果を見るのは、単語同士の類似度比較と、単語の組み合わせでの類似度(足し算引き算の結果)比較を行いました。
- 単語類似度
問題
ランク | cbow 1 | skip−gram 1 | cbow 10 | skip−gram 10 |
---|---|---|---|---|
1 | 弊害 | 解決 | 課題 | 解決 |
2 | 課題 | 課題 | 矛盾 | 課題 |
3 | 不都合 | コンフリクト | 不都合 | 難問 |
明日
ランク | cbow 1 | skip−gram 1 | cbow 10 | skip−gram 10 |
---|---|---|---|---|
1 | きっと | あした | いつか | あした |
2 | あした | 明後日 | きっと | あす |
3 | 君たち | きっと | あした | きっと |
- 単語の組み合わせ類似度
日本 + 東京 - アメリカ
ランク | cbow 1 | skip−gram 1 | cbow 10 | skip−gram 10 |
---|---|---|---|---|
1 | ロサンゼルス | ニューヨーク | ニューヨーク | ニューヨーク |
2 | ニューヨーク | ヨンカーズ | ロサンゼルス | ロサンゼルス |
3 | サンフランシスコ | ファロン | ロンドン | カリフォルニア |
昼 + 太陽 - 夜
ランク | cbow 1 | skip−gram 1 | cbow 10 | skip−gram 10 |
---|---|---|---|---|
1 | 満月 | 満月 | 満月 | 満月 |
2 | 夜空 | 夜空 | 夜空 | トラウィスカルパンテクートリ |
3 | 地平線 | まばゆい | 地平線 | ぃだ |
基本的にイテレーション回数が多い方が良いように見えますが、4番目の「昼 + 太陽 - 夜」を見ると、skip−gram 10の2,3番目の結果は全く意味が分かりません。「トラウィスカルパンテクートリ」はアステカ神話における明けの明星を擬人化した神様であるとのことです。「ぃだ」については全くわかりません。「てぃだ」であれば沖縄で太陽を意味する方言になりますが、mecab で分かち書きする時点でうまく処理できていないものと思われます。
また、「明日」の類似単語として「きっと」が現れたり、「日本 + 東京 - アメリカ 」の結果に「ワシントン」が出てこないなど、必ずしも意図していない結果が出てきています。このあたりのアルゴリズムやコーパスと欲しい結果とのギャップを埋めるようなワンクッションが、学習過程か学習結果の利用時にまだまだ必要そうです。
word2vec を青空文庫で試してみる
word2vec は単語のベクトル表現をえるための手法の一つで、ニューラルネットワークを利用して行われているものです。 登場した当時の他の単語ベクトル生成手法に比べ高速に、そして単語関係の表現能力が高い獲得できる点がポイントです。 CBOWとskip−gramの2つのアルゴリズムが提案されていて、今回はCBOWを使ってみました。
CBOWは、注目する単語の周辺 N 語を入力すると、注目する単語にカテゴライズするように学習します。 ニューラルネットワークの形としては、やや変則的ですが、入力層がNxV(Vは単語数)、出力層がVの2層のニューラルネットワークになります。 入力層から隠れ層への重み行列は単語ごとに共通で、単語表現を合計した後、V次元の入力層に入力するのと同じになります。 入力層や出力層での単語表現は、IDの割り当てになります。その単語を表現する次元は1、それ以外は0になっているベクトルです。 一方、学習の結果得ることができる単語ベクトルは、入力層から隠れ層への重み行列を用います。 このことにより、学習器が獲得した内部表現を利用することができるようになります。
ちなみに今回は、chainer のword2vecサンプルではなく、C言語実装のword2vecを利用しました。 理由としては、単純にこっちの方が非常に高速だからです。こちらのブロク記事で一目瞭然ですが、C言語実装のword2vecはCPUだけで非常に高速に動作します。 C言語で実装されているということと、頻出語の学習を極力行わないようにするサブサンプリングという処理を行っているのが大きいのだろうと思います。
データには、青空文庫のテキストデータを使用しました。特定の作家の作品を全て落としてきて、青空文庫用の特殊記法やヘッダー、フッターを以下のようなコードで削って利用しました。 また、テキストを分かち書き形式にしておく必要がありますが、それにはmecabを利用しています。
import re import argparse # 文中の特殊記号の正規表現 replace_regex = [ re.compile("《.+?》"), re.compile("|"), re.compile("[#.+?]"), re.compile("〔.+?〕") ] def aozora2txt(input, output): f_in = open(input, 'r', encoding='shift_jis') f_out = open(output, 'a', encoding='utf-8') # 本文推定用 start_line = "-------------------------------------------------------" end_word = "底本:" start_line_count = 0 for line in f_in: # 終了と開始を判定する if line.find(end_word) > -1: break if line.find(start_line) > -1: start_line_count += 1 continue if start_line_count < 2: continue for regex in replace_regex: line = regex.sub('', line) # 本文中の特殊記号を除去 f_out.write(line) f_in.close() f_out.close()
作家は、夏目漱石と太宰治。類似単語を検索する単語には、人生、愛。 パラメーターによる結果比較のため、出力するベクトルの次元数と、サブサンプリングに利用する値を変えてみています。ベクトルの次元数を変えることで獲得される内部表現の形が、サブサンプリングに利用する値を変えることで頻出語の扱われ方がそれぞれ影響を受けます。
- 人生
ランク | ベクトル100 サンプリング1e-2 |
ベクトル200 サンプリング1e-2 |
ベクトル100 サンプリング1e-4 |
ベクトル200 サンプリング1e-4 |
---|---|---|---|---|
1 | 人世 | 人世 | 触れろ | 意義 |
2 | 自己 | 宇宙 | 意義 | 人世 |
3 | 宇宙 | 人格 | 美的 | 触れろ |
ランク | ベクトル100 サンプリング1e-2 |
ベクトル200 サンプリング1e-2 |
ベクトル100 サンプリング1e-4 |
ベクトル200 サンプリング1e-4 |
---|---|---|---|---|
1 | 人間 | 奇蹟 | ドラマ | ドラマ |
2 | 愛 | 青春 | 做 | 做 |
3 | 行為 | 行為 | 現実 | 青春 |
- 愛
ランク | ベクトル100 サンプリング1e-2 |
ベクトル200 サンプリング1e-2 |
ベクトル100 サンプリング1e-4 |
ベクトル200 サンプリング1e-4 |
---|---|---|---|---|
1 | 恋 | 恋 | 弄ぶ | 対象 |
2 | 情 | 個々 | 純潔 | 深刻 |
3 | 宇宙 | 宇宙 | 信念 | 恋 |
ランク | ベクトル100 サンプリング1e-2 |
ベクトル200 サンプリング1e-2 |
ベクトル100 サンプリング1e-4 |
ベクトル200 サンプリング1e-4 |
---|---|---|---|---|
1 | 美 | 美 | 遂行 | 遂行 |
2 | 正義 | 愛情 | 異性 | 異性 |
3 | 愛情 | 信実 | 愛情 | 表現 |
結果としては、作家と頻出語の扱いの違いが、類似語の探索結果に大きく影響を与えているようです。思ったより出力次元数には影響がないようですね。一作家の文章だけなので全体として語彙が少なめ(一万五千語くらい)だからかもしれません。
Chainer の imagenet サンプルで遊んでみる
Deep Learning といえば、やはり画像認識での利用です。今さら感もないではないですが、chainer の imagenet サンプルは、日々進歩している画像認識処理を様々な形のネットワークで試してみることができ、Deep Learning の仕組みがどのようになっているか、chainer でオリジナルのネットワークを組み立てるにはどのように記述したらいいか、概要を把握するにはぴったりのサンプルだと思います。 今回は、chainer 1.7.1 に付属している imagenet サンプルのうち NIN ネットワークサンプルを利用して画像認識を行ってみました。 画像データセットとして、こちらの256 Object Categoriesを使用しました。
Deep Learning で画像認識を行う場合、一般的に畳み込みニューラルネットワーク(CNN)と呼ばれるものを用います。ネットワークの名前にもなっている畳み込み処理ですが、Deep Learning 特有の処理ではなく、画像処理として一般的に用いられているフィルタ処理のことを指します。このフィルタ処理は、数学的には行列演算として表現することができ、前層の出力に重みをかけ合計することにより次層の入力とする、ニューラルネットワークの演算処理でそのまま表現することができます。 CNN では、学習によりこの重みが更新される、つまり画像を処理するためによりよいフィルタを獲得していくことが成果となります。また、フィルタは何層にも重ねることができ、フィルタの出力結果に対するフィルタといったより複雑な特徴抽出を行うことができます。
このフィルタ処理の結果は、似たような画像であっても微妙な違いにより出力にムラが出ることがあります。そのムラを除去するために用いられるのがプーリング層で、出力結果を安定させる効果があります。この層は、特定区間の平均や最大を出力とする固定の操作が行われるため、学習による重みの更新を行う必要はありません。プーリング層は、一般的に畳み込み層の直後に置かれます。
今回使用したNINは、畳み込み層として、MLPConv と呼ばれる非線形な畳み込み層を使用しています。具体的には、3つの畳み込み層の後に1つのプーリング層があるという構造になります。NINは、このMLPConv層を4つつなげることにより、複雑な特徴表現を学習するようなネットワークになっています。
imagenet サンプルでは、学習データの用意は自分で実装する必要があります。画像は適当な画像データベースからおとしてくれば良いですが、学習に用いるデータセットのリストの作成と、学習データを256x256サイズにリサイズしておく必要もあります。今回は、ラベル毎にランダムで、80%を学習用データ、残りを検証用データとして扱うようにしました。また、縦横それぞれが256ピクセルになるように拡大・縮小することにより、画像をCNNで扱えるようにリサイズしています。
import os import argparse import random from PIL import Image parser = argparse.ArgumentParser(description='Image net dataset create') parser.add_argument('--root', '-r', default='.', help='image files root path') parser.add_argument('--output', '-o', default='Images', help='output root path') args = parser.parse_args() train_list_file = open('train_list.txt', mode='w', encoding='utf-8') validate_list_file = open('validate_list.txt', mode='w', encoding='utf-8') train_rate = 0.8 # 80% の確率で学習用データとする if not os.path.lexists(args.output): os.mkdir(args.output) directories = os.listdir(args.root) for i, directory in enumerate(directories): full_dir_path = os.path.join(args.root, directory) if not os.path.isdir(full_dir_path): continue for file_name in os.listdir(full_dir_path): try: # Resize 256x256 input_path = os.path.join(full_dir_path, file_name) if not os.path.isfile(input_path): continue path = os.path.join(args.output, file_name) Image.open(input_path).resize((256, 256), Image.LANCZOS).convert("RGB").save(path) label = directory.split(".")[0] line = path + " " + label + "\n" if train_rate > random.random(): train_list_file.write(line) else: validate_list_file.write(line) except: print(file_name, "is not image") train_list_file.close() validate_list_file.close()
ちょっと前置きが長くなりましたが、早速学習結果を見てみましょう。まず、30エポック学習させた結果どのような画像を認識できるようになったか見てみます。ラベルが256ありますが、そのうち野球用バッドの画像セットについての結果です。バッドといえば言わずもがなこんな感じの画像が含まれています。
一方で、下のようなバッド以外のものも混じった意地悪なデータも学習セットにはだいぶ含まれています。
このような画像を認識した結果、基本的に学習セットにあるデータに関しては、バッドと認識するようになりました。混合画像までバッドと認識してしまうのはちょっと考えものですが、そのように教え込んでいるので仕方ありません。一方、未学習データに関しては下の一番目や三番目の画像はバッドと認識してくれますが、二番目の画像は残念ながらバッドとしては認識してくれませんでした。なんとなくですが、向きが関係してるのではないかと予想しています。
次に、識別精度を見てみます。比較として同じデータで別に学習し、10エポックで学習を終了したモデルの結果も掲載します。見ての通り、学習用データにおいても、検証用データにおいても、30エポックまで学習したモデルの方が識別精度が良くなっています。 学習用データセットの識別精度は大幅に上昇しており、ほとんどの画像を正しくラベリングできるようになっています。しかし、先ほどのバッド例のように、他の物体も混じっている画像なども多く含まれているため、学び方としてはあまり良くないように思われます。 検証用データの精度があまり上昇していなことからもそのあたりは読み取っていただけるかなと思います。もうちょっとスクリーニングされているデータセットでないと、適当にやるには厳しかったかもしれません。
30 epoch result: train accuracy: 0.926514966153 validate accuracy: 0.422843056697 10 epoch result: train accuracy: 0.390098686893 validate accuracy: 0.315529991783
最後に、上で比較したそれぞれのモデルの第一層の重みを可視化したものを比較してみました。左がエポック10まで学習したもの、右がエポック30まで学習したものとなります。なんとなくですが、右の方が濃淡がはっきりしているように見えます。しっかり特徴抽出を行えるようになっているということですね。ここまで自動で識別できることを考えると、多少複雑な識別問題でもそれなりのデータセットを用意すれば的確に認識してくれそうです。