AtCoder Beginner Contest 161 D - Lunlun Numberの考え方、Pythonでの解き方

※この記事はjohannyjm1さんに教えたもらった解き方を自分のために整理し直したものです。

元の問題

atcoder.jp

考え方

まず1 ~ 9までの数は必ずルンルン数となります。

次に1を取り出します。 ルンルン数の定義は「隣り合うどの 2 つの桁の値についても、差の絶対値が 1 以下」のため、 1に-1, 0, 1を加えた数字を繋げたものが次のルンルン数になります。

10,11,12の数字を最後に追加します。

2についても同様に行うと、下記のような状態になります。 問題文中の入力例1については、小さい方から 15 番目までのルンルン数を求める問題のため、答えは23であることが分かります。

この考え方では、最初に1 ~9の数字を順に見ていき、追加する数字を探索する順序を(-1,0,1)にすることで、数字の登場順がルンルン数の昇順になるようにしています。

取り出す数字が2桁である10になった場合を考えます。 10の下一桁である0に対して、-1, 0, 1の計算を行います。 この時、0から-1を引いた値は対象外となります。

これの繰り返しで解けるはずです。

上記は幅優先探索の考え方であり、queueに対しFIFO(First In First Out)を行うことで実装できます。 この

Pythonコード

※こちらのコードはjohannyjm1さんの下記提出のコードをベースにしています。

atcoder.jp

PythonでqueueのFIFOを実装するためには、dequeのappendとpopleftを用います。

コード上にコメントで説明を記載しています。

from collections import deque


def main():
    k = int(input())

    cnt = 9

    if k <= cnt:
        print(k)
        exit()

    q = deque(list(range(1, 10)))
    while len(q) > 0:
        # queueの一番左の値を出力
        v = q.popleft()

        for diff in (-1, 0, 1):
            # vから下一桁を取得するために%10を行う
            nv = v % 10 + diff

            # nvの値が0より小さい、もしくは10以上の場合は計算しない
            if nv < 0 or nv >= 10:
                continue

            # 例えば、1に2を繋げた12を出力したい場合、1(ここではv)に対し10をかける必要がある
            next_num = v * 10 + nv
            # queueの一番右の値を入力
            q.append(next_num)
            cnt += 1

            # 問題文で指定されたk回の数字を出力
            if cnt == k:
                print(next_num)
                exit()


if __name__ == "__main__":
    main()

2022年4 ~ 6月の個人OKR

これは何

4 ~ 6月の個人のOKRです。毎月更新する形で振り返ります。

前期の結果・反省

詳細はこちら

  • Objective1: 機械学習を活用したデモをさくっと作れるようになる。0.5
  • KR3: 「ビッグデータ分析・活用のためのレシピ」を読み、SQLを自由に使えるようにする 。0.0

    • 未着手
  • Objective2: エンジニアとして必要な知識を幅広く身につける 0.5

    • KR1: Webアプリを作る知識を得るために、progateのHTML、CSSRubyRuby on Railsのコースを終える。 0.8
      • progateのHTML、CSSRuby完了
      • progateのRuby on Railsは、現在2チャプター(全5チャプター)の途中。
    • KR2: 線形代数の本を1冊以上読む。0.0
      • 未着手
    • KR3: Linuxに関する本を1冊以上読み、得た知識を実務で活用する。 0.8
      • 「新しいLinuxの教科書」を75%読んだ。
  • Objective3: 人に自慢できるレベルのベースの演奏技術を身につける 0.2

    • KR1: 休日に10分、ベースの基礎練習をする 0.0
      • できなかった
    • KR2: 毎日5分、次回投稿曲の練習をする 0.5
      • 3日に一回程度、10分はできている
    • KR3: 個人の演奏動画を1本、Youtubeにuploadする 0.0
      • 骨折の関係で投稿が1ヶ月遅れる予定
  • Objective4: 長く働けるよう健康な体を手に入れる 0.5

    • KR1: 週に1回、少なくとも3~4km走る 0.0
      • 骨折もあり不可能
    • KR2: 「腕、腹筋、背筋、足、体幹」のどれか一つの筋トレを2日に1回行う 1.0
      • 平均週4回は筋トレをできている
    • KR3: 10時までに布団に入り、5時に起きる 0.7
      • 11時寝の6時起きはほぼできるようになった

総合得点:0.4 感想: - やることを多くし過ぎた。次Qはもっと限定する。 - 何をいつまでにやるかをもっと具体的にすべきだった。

今期の目標

  • Objective1: 数学・アルゴリズムに強くなる
    • KR1: 5月末までに、「問題解決のための「アルゴリズム×数学」が基礎からしっかり身につく本」を読み切る
    • KR2: 6月末までに、「アルゴリズムとデータ構造」を読み切る
    • KR3: 5月からABCに隔週で出場する
    • KR4: 参加したABCに関しては、全問復習して解ききる
    • KR5: 6月末までに学んだ知識を業務に活用する
  • Objective2: 機械学習に強くなる
  • Objective3:機械学習を活用したWebアプリを作る
    • KR1: 4月末までに、Ruby on Railsのprogateの講座を終える
    • KR2: 5月末までに、HTML, CSS, Ruby, Ruby on Railsのpaizaのスキルチェック問題を解き、Cランクまでクリアする
    • KR3: 6月末までに、既存のwebアプリのクローンアプリを作って、Herokuで公開する
  • Objective4:エンジニアとして幅広い知識を身に付ける
    • KR1: 4月末までに「新しいLinuxの教科書」を読了する
    • KR3: 6月末までに「ビッグデータ分析・活用のためのレシピ」を読み、SQLを自由に使えるようにする 。
  • Objective5: 人に自慢できるレベルのベースの演奏技術を身につける 0.
    • KR1: 毎日10分、次回投稿曲の練習をする
    • KR2: 週末に1回、基礎練習を10分行う
    • KR3: 5月末までに個人の演奏動画を1本、Youtubeにuploadする
  • Objective6: 長く働けるよう健康な体を手に入れる 0.5
    • KR1: 「腕、腹筋、背筋、足、体幹」のどれか一つの筋トレを2日に1回行う
    • KR2: 10時までに布団に入り、5時に起きる

ざっくり言うと4月はKaggleに集中して、5月からAtcoder, ML周りの勉強、Webアプリ作成に注力します。何だか前期よりやることが増えている気がしますが、一旦これで。

0723追記

結果に関して記載します。OKR方式でやるより、自分が今したい勉強を気が向くままにやった方が精神衛生上良さそうと感じたので、途中から気にしていませんでした。

  • Objective1: 数学・アルゴリズムに強くなる

    • KR1: 5月末までに、「問題解決のための「アルゴリズム×数学」が基礎からしっかり身につく本」を読み切る
      • 30%程度よみました
    • KR2: 6月末までに、「アルゴリズムとデータ構造」を読み切る
    • 0%です
      • KR3: 5月からABCに隔週で出場する
        • 50%、隔週ではありませんが、数回参加できました
      • KR4: 参加したABCに関しては、全問復習して解ききる
        • 70%、D問題まで解けるようになる事を目標にし、Dまでは復習しました
    • KR5: 6月末までに学んだ知識を業務に活用する
      • 50%、活かした場面もありました。
  • Objective2: 機械学習に強くなる

    • KR1: 5月3日までKaggleのNBMEに参加し、金メダルを取る
      • 参加し、チームメイトのおかげで銅メダルでした
    • KR2: 5月末までに、「機械学習のエッセンス」を読了する
      • 80%、もうすぐ終わりそう
    • KR3: 6月末までに、2021年度 離散数学入門 〜グラフ理論の世界にようこそ〜 を全部見る
      • 0%、全く見れていません
    • KR4: 6月末までに、「実践 自然言語処理」を読了する
      • 50%、現在6章です
    • KR5: 6月末までに不均衡データに関する論文を5本以上読む
      • 100%、不均衡データ論文読み会を開いていたため、5~6本程度読みました
  • Objective3:機械学習を活用したWebアプリを作る
    • KR1: 4月末までに、Ruby on Railsのprogateの講座を終える
      • 80%,まだ途中です。
    • KR2: 5月末までに、HTML, CSS, Ruby, Ruby on Railsのpaizaのスキルチェック問題を解き、Cランクまでクリアする
      • 0%
    • KR3: 6月末までに、既存のwebアプリのクローンアプリを作って、Herokuで公開する
      • 0%
  • Objective4:エンジニアとして幅広い知識を身に付ける
    • KR1: 4月末までに「新しいLinuxの教科書」を読了する
      • 100%、読了です
    • KR3: 6月末までに「ビッグデータ分析・活用のためのレシピ」を読み、SQLを自由に使えるようにする 。
      • 0%
  • Objective5: 人に自慢できるレベルのベースの演奏技術を身につける 0.
    • KR1: 毎日10分、次回投稿曲の練習をする
      • 100%
    • KR2: 週末に1回、基礎練習を10分行う
      • 0%
    • KR3: 5月末までに個人の演奏動画を1本、Youtubeにuploadする
      • 100%
  • Objective6: 長く働けるよう健康な体を手に入れる 0.5
    • KR1: 「腕、腹筋、背筋、足、体幹」のどれか一つの筋トレを2日に1回行う
      • 80%
    • KR2: 10時までに布団に入り、5時に起きる
      • 10%

2021年振り返り、2022年の人生OKR

これは何

2021年に何が出来たかを振り返って、2022年はどうしていきたいか決めます。

2021年にできたこと

優先度が大きく変わった

Twitter上で2021年にやりたいこととして策定していたのは下記です。 ただし、全力で仕事に取り組み、成果を出したくなってしまったので優先度を大きく変更することにしました。

この中だと、「画像コンペでメダル」「技術記事を定期的に書く」だけは達成できたかなと思います。

2021やったこと・身についたことざっくりリスト

意味があったと思えるものだけ抜粋

  • NLPを用いたシステム開発、ABテスト、運用

  • NLP関連の本2.5冊、NLP100本ノック(90問ぐらい)、NLP関連論文約20本ぐらい読んだ

    • 放送大学 自然言語処理
    • 機械学習・深層学習による自然言語処理入門
    • ゼロから作るDeep Learning2 ―自然言語処理編(途中)
    • 論文はPrivateで読んだ下記に加え、業務で数本読んだ
  • プロダクトレベルのPythonの書き方

    • 実務である程度習得(修行中)
  • 統計・A/Bテスト関連の本2.5冊

    • Rによるやさしい統計学
    • サンプルサイズの決め方
    • A/Bテスト実践ガイド(途中)
  • docker, gitの基礎知識の習得

    • 米国AI開発者がゼロから教えるDocker講座
    • 米国AI開発者がやさしく教えるGit入門講座
    • 上記を参考にしながら実務で習得
  • AWSの最低レベルの知識習得

    • 図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書
    • 上記を参考にしながら実務で習得
  • 「何から取り組むべきか」の考え方の本3冊

    • イシューからはじめよ
    • 問題解決の全体観 上巻 ハード思考編
    • Measure What Matters(メジャー・ホワット・マターズ) 伝説のベンチャー投資家がGoogleに教えた成功手法 OKR
  • 生産性向上関連の本5冊

    • PDCAノート(継続して実践中)
    • 独学大全 絶対に「学ぶこと」をあきらめたくない人のための55の技法
    • どんな仕事も「25分+5分」で結果が出る
    • 自分を操る超集中力
    • 学びを結果に変えるアウトプット大全
  • 生き方に関する本2冊

    • 嫌われる勇気
    • 幸せになる勇気
  • バンドの演奏動画をYoutubeに2本投稿

  • 筋トレ開始、8kg痩せた

こうやってみると、NLP全般の習得、仕事で必要な最低知識の習得に時間を割いていたなぁと感じます。ただ、成果はあまり出せなかった点が要反省です。今期は結果出す。

2022年1月~3月にプライベートで取り組むこと

OKR形式で書いてみます。

  • Objective1: 機械学習を活用したデモをさくっと作れるようになる。

    • KR1: NLP、グラフ、ABテストに関する本を1冊以上読み、実務で活用する。
    • KR2: streamlitを用いた簡単なデモを作成し、実務で活用する。
    • KR3: 「ビッグデータ分析・活用のためのレシピ」を読み、SQLを自由に使えるようにする 。
  • Objective2: エンジニアとして必要な知識を幅広く身につける

    • KR1: Webアプリを作る知識を得るために、progateのHTML、CSSRubyRuby on Railsのコースを終える。
    • KR2: 線形代数の本を1冊以上読む。
    • KR3: Linuxに関する本を1冊以上読み、得た知識を実務で活用する。
  • Objective3: 人に自慢できるレベルのベースの演奏技術を身につける

    • KR1: 休日に10分、ベースの基礎練習をする
    • KR2: 毎日5分、次回投稿曲の練習をする
    • KR3: 個人の演奏動画を1本、Youtubeにuploadする
  • Objective4: 長く働けるよう健康な体を手に入れる

    • KR1: 週に1回、少なくとも3~4km走る
    • KR2: 「腕、腹筋、背筋、足、体幹」のどれか一つの筋トレを2日に1回行う
    • KR3: 10時までに布団に入り、5時に起きる

定期的に修正するかもしれません。2022年4月に振り返ります。

(220201追記) 2022年1月の進捗

それぞれの項目を0.0 ~ 1.0で評価してみます。シンプルに考えると現時点で0.3になってない項目はまずい。

  • Objective1: 機械学習を活用したデモをさくっと作れるようになる。
    • KR1: NLP、グラフ、ABテストに関する本を1冊以上読み、実務で活用する。 0.3
      • 簡単なグラフ本「グラフ理論とGraph Neueal Networks 概論」を1冊読み終えた。
      • 理解度は低いため、追加でグラフ本を一冊読む
    • KR2: streamlitを用いた簡単なデモを作成し、実務で活用する。 0.0
      • 未着手
  • KR3: 「ビッグデータ分析・活用のためのレシピ」を読み、SQLを自由に使えるようにする 。0.0

    • 未着手
  • Objective2: エンジニアとして必要な知識を幅広く身につける

    • KR1: Webアプリを作る知識を得るために、progateのHTML、CSSRubyRuby on Railsのコースを終える。 0.1
      • progateのHTML・CSS初級編をほぼ終えた。
    • KR2: 線形代数の本を1冊以上読む。0.0
      • 未着手
    • KR3: Linuxに関する本を1冊以上読み、得た知識を実務で活用する。 0.3
      • 「ふつうのLinuxプログラミング第2版」を33%読んだ。
  • Objective3: 人に自慢できるレベルのベースの演奏技術を身につける

    • KR1: 休日に10分、ベースの基礎練習をする 0.1
      • ほぼやれていない
    • KR2: 毎日5分、次回投稿曲の練習をする 0.1
      • ほぼやれていない
    • KR3: 個人の演奏動画を1本、Youtubeにuploadする 0.0
      • 未着手
  • Objective4: 長く働けるよう健康な体を手に入れる

    • KR1: 週に1回、少なくとも3~4km走る 0.0
      • ほぼやれていない
    • KR2: 「腕、腹筋、背筋、足、体幹」のどれか一つの筋トレを2日に1回行う 0.3
      • 平均週4回は筋トレをできている
    • KR3: 10時までに布団に入り、5時に起きる 0.1
      • ほぼやれていない

できていない目標は守り、現実的でない目標は修正かけていきます。

(220301追記) 2022年2月の進捗

それぞれの項目を0.0 ~ 1.0で評価してみます。現時点で0.6になってない項目はまずい。

  • Objective1: 機械学習を活用したデモをさくっと作れるようになる。
  • KR3: 「ビッグデータ分析・活用のためのレシピ」を読み、SQLを自由に使えるようにする 。0.0

    • 未着手
  • Objective2: エンジニアとして必要な知識を幅広く身につける

    • KR1: Webアプリを作る知識を得るために、progateのHTML、CSSRubyRuby on Railsのコースを終える。 0.5
      • progateのHTML、CSS完了
      • progateのRubyは、現在2チャプター(全5チャプター)の途中。
    • KR2: 線形代数の本を1冊以上読む。0.0
      • 未着手
    • KR3: Linuxに関する本を1冊以上読み、得た知識を実務で活用する。 0.6
      • 「新しいLinuxの教科書」を45%読んだ。
  • Objective3: 人に自慢できるレベルのベースの演奏技術を身につける

    • KR1: 休日に10分、ベースの基礎練習をする 0.1
      • ほぼやれていない
    • KR2: 毎日5分、次回投稿曲の練習をする 0.3
      • 3日に一回程度はできている
    • KR3: 個人の演奏動画を1本、Youtubeにuploadする 0.0
      • 未着手
  • Objective4: 長く働けるよう健康な体を手に入れる

    • KR1: 週に1回、少なくとも3~4km走る 0.0
      • ほぼやれていない
    • KR2: 「腕、腹筋、背筋、足、体幹」のどれか一つの筋トレを2日に1回行う 0.6
      • 平均週4回は筋トレをできている
    • KR3: 10時までに布団に入り、5時に起きる 0.3
      • 11時寝の6時起きはほぼできるようになった

できていない目標は守り、現実的でない目標は修正かけていきます。

(220401追記) 前Qの振り返り

  • Objective1: 機械学習を活用したデモをさくっと作れるようになる。0.5
  • KR3: 「ビッグデータ分析・活用のためのレシピ」を読み、SQLを自由に使えるようにする 。0.0

    • 未着手
  • Objective2: エンジニアとして必要な知識を幅広く身につける 0.5

    • KR1: Webアプリを作る知識を得るために、progateのHTML、CSSRubyRuby on Railsのコースを終える。 0.8
      • progateのHTML、CSSRuby完了
      • progateのRuby on Railsは、現在2チャプター(全5チャプター)の途中。
    • KR2: 線形代数の本を1冊以上読む。0.0
      • 未着手
    • KR3: Linuxに関する本を1冊以上読み、得た知識を実務で活用する。 0.8
      • 「新しいLinuxの教科書」を75%読んだ。
  • Objective3: 人に自慢できるレベルのベースの演奏技術を身につける 0.2

    • KR1: 休日に10分、ベースの基礎練習をする 0.0
      • できなかった
    • KR2: 毎日5分、次回投稿曲の練習をする 0.5
      • 3日に一回程度、10分はできている
    • KR3: 個人の演奏動画を1本、Youtubeにuploadする 0.0
      • 骨折の関係で投稿が1ヶ月遅れる予定
  • Objective4: 長く働けるよう健康な体を手に入れる 0.5

    • KR1: 週に1回、少なくとも3~4km走る 0.0
      • 骨折もあり不可能
    • KR2: 「腕、腹筋、背筋、足、体幹」のどれか一つの筋トレを2日に1回行う 1.0
      • 平均週4回は筋トレをできている
    • KR3: 10時までに布団に入り、5時に起きる 0.7
      • 11時寝の6時起きはほぼできるようになった

総合得点:0.4 感想: やることを多くし過ぎた。次Qはもっと限定する。

日本は他の国のKagglerよりTwitterが活発なのか

3行で

  • 最近、日本人Kagglerが強く・多くなっているのか、日本は他の国のKagglerよりTwitterが活発なのかを調べた。
  • 2021年12月時点で、KaggleRankTop100の中で最もユーザーが多い国は日本であり、ユーザー増加率、Tweet率も高い可能性がある。
  • 今後も日本人Kagglerの活躍を応援しています🙇‍♂️

目次

初めに

この記事はKaggle Advent Calendar 2021の18日目の記事です.

最近、日本人Kaggler強い気がしませんか? コンペが終わるたびに全日本人がメダルを取得し、Expert、Master、GMに昇格してませんか?私の気のせいでしょうか・・・。 また、それと同時に日本人Kagglerがどんどん増えている感覚もあります。

仮に、日本人Kagglerが最近強く、そして多くなっているのが本当だとしたら、他の国と比べて日本にはどのような特徴があるのでしょうか?

私は、その一因に「コミュニティの強さ」があると考えています。Kaggler-ja, 多くの勉強会・懇親会、Twitter上での交流など日本ではKaggleに関する多くのコミュニティが存在します。 もちろん外国にも同様のコミュニティが存在するのは知っていますが、観測しやすいTwitter上では、日本人の交流が非常に活発なように見えます。

本来はこの交流の活発さをネットワークの観点から調べたかったのですが、TwitterAPIなどの制限が厳しくネットワーク分析に辿り着くのが大変そうだったため、今回は下記の3点に絞り、簡単にできるレベルで確認しました。いつかもっとやりたい。

  • 最近、日本人Kagglerが強くなってきているのか
  • 最近、日本人Kagglerは多くなってきているのか
  • 日本は他の国のKagglerよりTwitterが活発なのか

実装

実装は下記です。gokartというpipelineを用いています。

github.com

最近、日本人Kagglerが強くなってきているのか

シンプルに考えると、KaggleRankの国ごとの平均が、年ごとにどのように推移しているかを見たいですね。 ただ、Meta Kaggleなど覗きましたがそのようなデータは見当たりませんでした。

しかし、2016年頃のKaggleRankがTop100のUserの国情報を取得しているNotebookがありました。

Top-100 Kaggle users by Country www.kaggle.com

この情報を活用させてもらいましょう。同様の情報を2021年12月14日に、Kaggle User Rankingを手動で一人一人確認して国の数を集計しました。 その数の上位10国を2016年、2021年それぞれでグラフ化します。

f:id:sinchir0:20211214222540p:plain
2016年と2021年のKaggleRankがTop100のUserの国の数、上位10国

2021年の情報だけでもかなり面白いですね、分かることをまとめます。

  • 2021年12月時点で、KaggleRankTop100の中で最もユーザーが多い国は日本である。
  • 2016年と比較し2021年では、日本人のtop100ユーザーは増加している。

一つ目、私は知りませんでした・・・、いつの間にという感じですね。2018年の情報とかも知りたいのですが、過去のRanking情報の取得ができなかったためここで断念です。

次に各国のユーザーが2021のRankingのどの辺りに所属しているかの分布を確認します。今回は上位2国であるJapanとUnited Statesで比較しましょう。

f:id:sinchir0:20211214221527p:plain
KaggleRankにおけるUnited StatesとJapanのヒストグラム

上記グラフから下記のことが分かります。

  • 2021年12月時点で、日本はKaggleRankTop100の後半に属しているユーザーが多い

そのため、Top50などに絞るとまた違った結果になると思われます。

最近、日本人Kagglerが増えているのか

全ユーザーの国情報を集計したいですが、データが見つかりませんでした。

しかし、Kaggleが毎年行っている調査に国別の回答者数のデータが公開されています。これを疑似的な国別のKaggleユーザー数と考え、日本のランキングがどう変化しているか確認しましょう。

下記Notebookが国ごとのランキングを出力しているため、お借りして日本のデータを確認してみます。

A deep dive into the Kaggle Survey from 2017-2021✨ www.kaggle.com

f:id:sinchir0:20211207073523p:plain
Kaggle Machine Learning & Data Science Surveyにおける日本の回答者数の推移

グラフを見ると下記のことが分かります。

  • 2017年では回答者数Top10に入っていなかった日本が、2021年に向かって順位を10→4→4→3と上げている
  • 日本は他の国よりもユーザーの増加率が高い可能性がある。

これまた日本凄いって感じですね・・・Kaggle大国と呼ばれる日も近いかも、というかもう来ている?

日本は他の国のKagglerよりTwitterが活発なのか

では、Twitterが活発なのかどうかを調べていきたいと思います。

理想としては「国ごとのKagglerがどの程度Twitter上で発信しているか」を集計したいです。

しかし、TwitterAPIではProfileに対するキーワード検索が(試した限りでは)出来ず、Profileに"Kaggle"という文字列を含むアカウントを特定することができませんでした。

そのため、なんとなく国とTweet活発さが分かる手法として「直近3週間の"Kaggle"を含むTweetを全て取得し、その言語を判定する」という方法を取ります。

直近3週間の"Kaggle"を含むTweetの言語判定

データ集計

データ集計期間は11/26 ~ 12/17です。その中で、"Kaggle"と含まれるTweetを下記条件で取得しています。

  • Tweet,、Reply、引用Retweetを集計し、Retweetは除外。
  • Reply先のuser_nameに"Kaggle"という文字列が含まれている場合を除外。

データ件数は、5126件となりました。

言語判定の方法

下記記事「タイトルの言語判定特徴」から、fastTextによる言語判定の方法をお借りします。

www.guruguru.science

実装はこんな感じ

最近遅くないpandasの書き方という記事でpandas.applyよりもnumpy.vectorizeの方が速いと見たので早速使ってみています。

class AddClassifyLangColTask(GokartTask):

    preprocess_task = gokart.TaskInstanceParameter()

    fasttext_path: str = luigi.Parameter()

    def requires(self):
        return self.preprocess_task

    def classify_lang(self, model, text: str) -> str:
        return model.predict(text)[0][0][-2:]

    def run(self):

        df = self.load_data_frame()

        model = load_model(self.fasttext_path)

        df["lang"] = np.vectorize(self.classify_lang)(model, df["clean_text"])

        df["lang"] = np.where(
            ((df["lang"] != "en") & (df["lang"] != "ja")), "others", df["lang"]
        )

        self.dump(df)

判定結果

言語判定結果は下記のようになりました。

f:id:sinchir0:20211218002208p:plain
取得Tweetの言語割合

絶対数として日本語が最も多いということが分かります。 しかし、この情報だけではTwitterのそもそもの利用者数が国ごとに違うため、正確な比較ではありません。

例を出すと、現在は

  • Twitterユーザーが10万人の言語で1500tweetされたこと
  • Twitterユーザーが1万人の言語で1500tweetされたこと

を比較しているのと同じ意味になります。これは公平な比較とは言えません。 そのため、「Kagglerでもあり、Twitterユーザーでもあるユーザー数」を言語ごとに知り、その数でTweet数を割りたいです。

しかし、「Kagglerでもあり、Twitterユーザーでもあるユーザー数」は取得する方法が思いついていません。

そこで、Kagglerの条件は外し、「Twitterユーザーの総数」のみで計算を行います。 ここでは、「Kagglerでもあり、Twitterユーザーでもあるユーザー数」は「Twitterユーザーの総数」に比例する、という仮定をおきます。

Twitterユーザーの総数」として、Twitterの国別MAU(Monthly Active User)を利用します。 Monthly Active Userは、1ヶ月の間に何人のユーザーが利用したかを表す指標になります。

(言語ごとのTweet数 / 言語ごとのユーザー数)を計算することで、「Twitterユーザーの総数」を考慮した数字で比較を行います。

今回は簡易的に実施するため、下記のような仮定を置きます。もちろん現実は色々な国で多様な言語が使われています。

  • 英語 : U.SのMAUを利用
  • 日本語 : 日本のMAUを利用

MAUは下記のサイトの数字を利用しました。

www.statista.com

サイトより、2021年10月時点でのUnited StatesのMAUは77.75million(7775万人)、日本は58.2million(5820万人)となっています。 2021年11~12月でも大まかな数字は変わってないだろうということで、英語・日本語の数字を上記の77.75million, 58.2millionで割ってみます。

f:id:sinchir0:20211218002341p:plain
1月当たり平均何回Kaggleが含まれるTweetをするか

Tweetの取得期間が3週間、MAUは1ヶ月のアクティブユーザーです。 分かりやすさのために「取得Tweet数/MAU」の数が「各ユーザーが1ヶ月当たりに平均して行うTweet数」と近似します。

上記近似を踏まえると、このグラフから、下記が分かります。

  • 日本語のTwitterユーザーは1ヶ月当たり平均0.000029回Kaggleが含まれるTweetをする
  • 英語のTwitterユーザーは1月当たり平均0.000020回Kaggleが含まれるTweetをする

・・・数字が小さすぎてよくわからないですね。直感的な数字ではありませんでした。

しかし、下記のことは分かります。

  • 日本語のTwitterユーザーは、英語のTwitterユーザーよりも1月当たりに"Kaggle"を含むTweetをする平均回数が多い

大雑把な仮定も多いですが、Kaggle関連のTweetは、日本語の割合も多く、日本人Kagglerが最もTwitterを積極的に利用している可能性があると言えそうです。

この勢いで今後もKaggleが強い国日本のイメージが定着するといいなと願います🙏

まとめ

下記のようなことが分かりました。

  • 2021年12月時点で、KaggleRankTop100の中で最もユーザーが多い国は日本である。
  • 2016年と比較し2021年では、日本人のtop100ユーザーは増加している。
  • 2021年12月時点で、日本はKaggleRankTop100の後半に属しているユーザーが多い
  • 2017年では回答者数Top10に入っていなかった日本が、2021年に向かって順位を10→4→4→3と上げている
  • 日本は他の国よりもユーザーの増加率が高い可能性がある。
  • 日本語のTwitterユーザーは、英語のTwitterユーザーよりも1ヶ月当たりに"Kaggle"を含むTweetをする回数が多い

最後に

変なところあったらTwitterか記事のコメントで教えてください。

twitter.com

言語処理100本ノック 2020 第9章を解きました。

言語処理100本ノック 2020の第9章を解いたので、その解法と思ったことをつらつら書きます。 学んだことを記録しているため、コード中にコメントが大量にあるのはご容赦ください。

コードはこちら

80. ID番号への変換

愚直にやっています。torchtextを使えばもっと簡単にできそう

# 80. ID番号への変換
# 問題51で構築した学習データ中の単語にユニークなID番号を付与したい.
# 学習データ中で最も頻出する単語に1,2番目に頻出する単語に2,……といった方法で,学習データ中で2回以上出現する単語にID番号を付与せよ.
# そして,与えられた単語列に対して,ID番号の列を返す関数を実装せよ.ただし,出現頻度が2回未満の単語のID番号はすべて0とせよ

import collections
import pickle
import sys

import pandas as pd

sys.path.append("../../src/NLP100")

from util import preprocess


def change_word_to_id(input_word: str, word_id_dict: dict) -> str:
    # ID番号へ変換、辞書に存在しないものは0をいれる
    result_list = []
    for word in input_word.split():
        if word in word_id_dict:
            result_list.append(str(word_id_dict[word]))
        else:
            result_list.append("0")

    return " ".join(result_list)


if __name__ == "__main__":

    # データの読み込み
    train = pd.read_csv("../../ch06/50/train.txt", sep="\t", index_col=0)
    test = pd.read_csv("../../ch06/50/test.txt", sep="\t", index_col=0)

    # trainとtestを結合する
    train["flg"] = "train"
    test["flg"] = "test"
    train_test = pd.concat([train, test])

    # 前処理
    train_test["TITLE"] = train_test[["TITLE"]].apply(preprocess)

    # 全文章を一つにまとめたstrを生成
    all_sentence_list = " ".join(train_test["TITLE"].tolist()).split(" ")

    # 全文章に含まれる単語の頻度を計算
    all_word_cnt = collections.Counter(all_sentence_list)

    # 出現頻度が2回以上の単語のみを取得
    word_cnt_over2 = [i for i in all_word_cnt.items() if i[1] >= 2]
    word_cnt_over2 = sorted(word_cnt_over2, key=lambda x: x[1], reverse=True)

    # 単語のみ取得
    word_over2 = [i[0] for i in word_cnt_over2]
    # ID番号を取得
    id_list = [i for i in range(1, len(word_over2))]

    # 単語とID番号をdictへとまとめる
    word_id_dict = dict(zip(word_over2, id_list))

    # 出力
    with open("word_id_dict.pkl", "wb") as tf:
        pickle.dump(word_id_dict, tf)

    # train_testのTITLEをID番号へと変換
    train_test["TITLE"] = train_test["TITLE"].apply(
        lambda x: change_word_to_id(x, word_id_dict)
    )

    # train, testへ分離
    train = train_test.query('flg == "train"')
    test = train_test.query('flg == "test"')

    # 出力
    train.to_pickle("train_title_id.pkl")
    train.to_csv("train_title_id.csv")

    test.to_pickle("test_title_id.pkl")
    test.to_csv("test_title_id.csv")

81. RNNによる予測

数式をコードとして反映するのにちょっと骨が折れました。

x = x[:, -1, :] # 一番最後の出力に絞る、やっていいのかこれ?というコメントありますが、結論としてやる必要があると考えています。 nn.RNNはsequentialに各単語の出力を吐き出し、それを全て保存しています。 今回必要なのは、一番最後の単語ベクトルの出力のため、x[:, -1, :]で取り出しています。

詳しくは公式のoutputを参照 (間違っていたら教えてください🙇‍♂️)

# 81. RNNによる予測
# https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html

import pickle

import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import Dataset


class TextDataset(Dataset):
    def __init__(self, X, y):
        self.X = X
        self.y = y

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        # Xをintのlistに変換
        X_list = [int(x) for x in self.X[idx].split()]

        # tensorに変換
        inputs = torch.tensor(
            X_list
        )  # .unsqueeze(0) # unsqueezeはtorch.Size([6]) → torch.Size([1, 6])
        label = torch.tensor(self.y[idx])

        return inputs, label


class RNN(nn.Module):
    def __init__(self, vocab_size, emb_dim=300, hidden_size=50, output_size=4):
        super().__init__()

        self.emb = nn.Embedding(
            vocab_size, emb_dim, padding_idx=0  # 0に変換された文字にベクトルを計算しない
        )

        self.rnn = nn.RNN(
            input_size=emb_dim,
            hidden_size=hidden_size,
            num_layers=1,
            nonlinearity="tanh",
            bias=True,
        )

        self.fc = nn.Linear(
            in_features=hidden_size, out_features=output_size, bias=True
        )

        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, h_0=None):
        x = self.emb(x)
        x, h_t = self.rnn(x, h_0)
        x = x[:, -1, :]  # 一番最後の出力に絞る、やっていいのかこれ?
        x = self.fc(x)
        x = self.softmax(x)
        return x


if __name__ == "__main__":

    # データの読み込み
    train = pd.read_pickle("../80/train_title_id.pkl")

    # yの変換
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    train["CATEGORY"] = train["CATEGORY"].map(cat_id_dict)

    # 辞書の読み込み
    with open("../80/word_id_dict.pkl", "rb") as tf:
        word_id_dict = pickle.load(tf)

    n_letters = len(word_id_dict.keys())
    n_hidden = 50
    n_categories = 4

    # modelの定義
    model = RNN(n_letters, n_hidden, n_categories)

    # datasetの定義
    dataset = TextDataset(train["TITLE"], train["CATEGORY"])

    # 先頭10個の結果を出力
    for i in range(10):
        X = dataset[i][0]
        X = X.unsqueeze(0)
        print(model(x=X))

    # tensor([[0.3996, 0.1701, 0.1483, 0.2821]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.2412, 0.2179, 0.1354, 0.4055]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.1941, 0.3746, 0.0832, 0.3481]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.1472, 0.3155, 0.1035, 0.4338]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.2272, 0.3158, 0.1757, 0.2812]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.2272, 0.3158, 0.1757, 0.2812]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.3958, 0.1784, 0.1419, 0.2839]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.3958, 0.1784, 0.1419, 0.2839]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.1054, 0.5542, 0.0961, 0.2442]], grad_fn=<SoftmaxBackward>)
    # tensor([[0.2882, 0.3606, 0.1956, 0.1556]], grad_fn=<SoftmaxBackward>)

82. 確率的勾配降下法による学習

SGDを用いて学習し、損失と正解率を表示するコードを追加しました。 ミニバッチ化は次の問題のため、BATCHSIZE=1を指定しています。

# 82. 確率的勾配降下法による学習
# 確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,問題81で構築したモデルを学習せよ.
# 訓練データ上の損失と正解率,評価データ上の損失と正解率を表示しながらモデルを学習し,
# 適当な基準(例えば10エポックなど)で終了させよ.

import os
import pickle
import random
import time

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def seed_everything(seed=42, use_torch=False):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    if use_torch:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True


seed_everything(use_torch=True)


class TextDataset(Dataset):
    def __init__(self, X, y):
        self.X = X
        self.y = y

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        # Xをintのlistに変換
        X_list = [int(x) for x in self.X[idx].split()]

        # tensorに変換
        inputs = torch.tensor(X_list)
        label = torch.tensor(self.y[idx])

        return inputs, label


class RNN(nn.Module):
    def __init__(self, vocab_size, emb_dim, hidden_size, output_size):
        super().__init__()

        self.emb = nn.Embedding(
            vocab_size, emb_dim, padding_idx=0  # 0に変換された文字にベクトルを計算しない
        )

        self.rnn = nn.RNN(
            input_size=emb_dim,
            hidden_size=hidden_size,
            num_layers=1,
            nonlinearity="tanh",
            bias=True,
            batch_first=True,
        )
        # batch_first=Trueにすると、inputとoutputの型が変わる

        # inputの想定の型を、(L, N, H_in)から(N, L, H_in)に変更する
        # L: Sequence Length
        # N: Batch Size
        # H_in: input size

        # outputの想定の型を、(L, N, D * H_out)から(N, L, D * H_out)に変更する
        # L: Sequence Length
        # N: Batch Size
        # D: 2 if bidirectional=True otherwise 1
        # H_out: hidden size

        self.fc = nn.Linear(
            in_features=hidden_size, out_features=output_size, bias=True
        )

        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, h_0=None):
        x = self.emb(x)
        x, h_t = self.rnn(x, h_0)
        x = x[:, -1, :]  # sequenceの長さ(現在は10)のうち、一番最後の出力に絞る、やっていいのかこれ?
        x = self.fc(x)
        x = self.softmax(x)
        return x


def train_fn(model, loader, optimizer, criterion, BATCHSIZE, HIDDEN_SIZE) -> float:
    """model, loaderを用いて学習を行い、lossを返す"""

    # 学習モードに設定
    model.train()

    train_running_loss = 0.0

    for dataloader_x, dataloader_y in loader:
        optimizer.zero_grad()

        dataloader_y_pred_prob = model(
            x=dataloader_x
            # x=dataloader_x,
            # h_0=torch.zeros(1 * 1, BATCHSIZE, HIDDEN_SIZE)
        )

        # dataloader_xでの損失の計算
        loss = criterion(dataloader_y_pred_prob, dataloader_y)
        # 勾配の計算
        loss.backward()
        optimizer.step()

        # 訓練データでの損失の平均を計算する
        train_running_loss += loss.item() / len(loader)

    return train_running_loss


def calculate_loss_and_accuracy(model, dataset, device=None, criterion=None):
    """損失・正解率を計算"""
    # 評価モードに設定
    model.eval()

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

    loss = 0.0
    total = 0
    correct = 0

    with torch.no_grad():
        for dataloader_x, dataloader_y in dataloader:
            # 順伝播
            outputs = model(dataloader_x)

            # 損失計算
            loss += criterion(outputs, dataloader_y).item()

            # 正解率計算
            pred = torch.argmax(outputs, dim=-1)
            total += len(dataloader_x)
            correct += (pred == dataloader_y).sum().item()

    return loss / len(dataset), correct / total


def padding(id_seq: str, max_len: int):
    """id_seqについて、
    max_lenより長い場合はmax_lenまでの長さにする。
    max_lenより短い場合はmax_lenになるように0を追加する。
    """
    id_list = id_seq.split(" ")
    if len(id_list) > max_len:
        id_list = id_list[:max_len]
    else:
        pad_num = max_len - len(id_list)
        for _ in range(pad_num):
            id_list.append("0")
    return " ".join(id_list)


def make_graph(value_dict: dict, value_name: str, bn: int, method: str) -> None:
    """value_dictに関するgraphを生成し、保存する。"""
    for phase in ["train", "test"]:
        plt.plot(value_dict[phase], label=phase)
    plt.xlabel("epoch")
    plt.ylabel(value_name)
    plt.title(f"{value_name} per epoch at bn{bn}")
    plt.legend()
    plt.savefig(f"{method}_{value_name}_bn{bn}.png")
    plt.close()


if __name__ == "__main__":

    # 時間の計測
    start = time.time()

    # データの読み込み
    train = pd.read_pickle("../80/train_title_id.pkl")
    test = pd.read_pickle("../80/test_title_id.pkl")
    test = test.reset_index(drop=True)

    # paddingの実施
    max_len = 10
    train["TITLE"] = train["TITLE"].apply(lambda x: padding(x, max_len))
    test["TITLE"] = test["TITLE"].apply(lambda x: padding(x, max_len))

    # yの変換
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    train["CATEGORY"] = train["CATEGORY"].map(cat_id_dict)
    test["CATEGORY"] = test["CATEGORY"].map(cat_id_dict)

    # 辞書の読み込み
    with open("../80/word_id_dict.pkl", "rb") as tf:
        word_id_dict = pickle.load(tf)

    N_LETTERS = len(word_id_dict.keys()) + 1  # pad分をplusする。
    EMB_SIZE = 300
    HIDDEN_SIZE = 50
    N_CATEGORIES = 4

    # modelの定義
    model = RNN(
        vocab_size=N_LETTERS,
        emb_dim=EMB_SIZE,
        hidden_size=HIDDEN_SIZE,
        output_size=N_CATEGORIES,
    )

    # criterion, optimizerの設定
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

    # datasetの定義
    dataset_train = TextDataset(train["TITLE"], train["CATEGORY"])
    dataset_test = TextDataset(test["TITLE"], test["CATEGORY"])

    # parameterの更新
    BATCHSIZE = 1
    dataloader_train = DataLoader(
        dataset_train, batch_size=BATCHSIZE, shuffle=False, drop_last=True
    )

    train_losses = []
    train_accs = []
    test_losses = []
    test_accs = []

    # deviceの指定
    device = (
        torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu")
    )

    EPOCH = 10
    for epoch in tqdm(range(EPOCH)):

        # 学習
        train_running_loss = train_fn(
            model, dataloader_train, optimizer, criterion, BATCHSIZE, HIDDEN_SIZE
        )
        print(train_running_loss)

        # 損失と正解率の算出
        train_loss, train_acc = calculate_loss_and_accuracy(
            model, dataset_train, device, criterion
        )
        test_loss, test_acc = calculate_loss_and_accuracy(
            model, dataset_test, device, criterion
        )

        train_losses.append(train_loss)
        train_accs.append(train_acc)

        test_losses.append(test_loss)
        test_accs.append(test_acc)

        # 20epoch毎にチェックポイントを生成
        if epoch % 20 == 0:
            torch.save(model.state_dict(), f"82_model_epoch{epoch}.pth")
            torch.save(
                optimizer.state_dict(),
                f"82_optimizer_epoch{epoch}.pth",
            )

    # グラフへのプロット
    losses = {"train": train_losses, "test": test_losses}

    accs = {"train": train_accs, "test": test_accs}

    make_graph(losses, "losses", bn=BATCHSIZE, method="rnn")
    make_graph(accs, "accs", bn=BATCHSIZE, method="rnn")

    print(f"train_acc: {train_acc}")
    print(f"test_acc: {test_acc}")

    # 時間の出力
    elapsed_time = time.time() - start
    print(elapsed_time)

学習結果はこんな感じ f:id:sinchir0:20210909075933p:plain

時間:390.43s

83. ミニバッチ化・GPU上での学習

BATCHSIZE=32を指定し、ミニバッチ化を行なっています。

# 83. ミニバッチ化・GPU上での学習
# 問題82のコードを改変し,B事例ごとに損失・勾配を計算して学習を行えるようにせよ(Bの値は適当に選べ).また,GPU上で学習を実行せよ.
# GPUで計算しているColabのコード
# https://colab.research.google.com/drive/1IAzvlHQ19RSkqyVk4UosinRUgXnu2Q59?usp=sharing
# dataset, h_0, modelをdevice送りした

import os
import pickle
import random

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def seed_everything(seed=42, use_torch=False):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    if use_torch:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True


seed_everything(use_torch=True)


class TextDataset(Dataset):
    def __init__(self, X, y):
        self.X = X
        self.y = y

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        # Xをintのlistに変換
        X_list = [int(x) for x in self.X[idx].split()]

        # tensorに変換
        inputs = torch.tensor(X_list)
        label = torch.tensor(self.y[idx])

        return inputs, label


class RNN(nn.Module):
    def __init__(self, vocab_size, emb_dim, hidden_size, output_size):
        super().__init__()

        self.emb = nn.Embedding(
            vocab_size, emb_dim, padding_idx=0  # 0に変換された文字にベクトルを計算しない
        )

        self.rnn = nn.RNN(
            input_size=emb_dim,
            hidden_size=hidden_size,
            num_layers=1,
            nonlinearity="tanh",
            bias=True,
            batch_first=True,
        )
        # batch_first=Trueにすると、inputとoutputの型が変わる

        # inputの想定の型を、(L, N, H_in)から(N, L, H_in)に変更する
        # L: Sequence Length
        # N: Batch Size
        # H_in: input size

        # outputの想定の型を、(L, N, D * H_out)から(N, L, D * H_out)に変更する
        # L: Sequence Length
        # N: Batch Size
        # D: 2 if bidirectional=True otherwise 1
        # H_out: hidden size

        self.fc = nn.Linear(
            in_features=hidden_size, out_features=output_size, bias=True
        )

        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, h_0=None):
        x = self.emb(x)
        # 普通のとき、torch.Size([32, 10, 300])
        # 落ちるとき, torch.Size([16, 10, 300])
        # train.shape[0] == 10672
        # 10672を32で割ると、333 余り16
        # よって余りをどう扱うのかの問題になる
        # loaderの引数にdrop_last=Trueを追加すると、余りは捨てるようになる。
        x, h_t = self.rnn(x, h_0)
        x = x[:, -1, :]  # sequenceの長さ(現在は10)のうち、一番最後の出力に絞る、やっていいのかこれ?
        x = self.fc(x)
        x = self.softmax(x)
        return x


def train_fn(
    model, loader, device, optimizer, criterion, BATCHSIZE, HIDDEN_SIZE
) -> float:
    """model, loaderを用いて学習を行い、lossを返す"""
    # 学習モードに設定
    model.train()

    train_running_loss = 0.0

    for dataloader_x, dataloader_y in loader:
        dataloader_x.to(device)
        dataloader_y.to(device)

        optimizer.zero_grad()

        dataloader_y_pred_prob = model(
            x=dataloader_x, h_0=torch.zeros(1 * 1, BATCHSIZE, HIDDEN_SIZE)
        )

        # dataloader_xでの損失の計算
        loss = criterion(dataloader_y_pred_prob, dataloader_y)
        # 勾配の計算
        loss.backward()
        optimizer.step()

        # 訓練データでの損失の平均を計算する
        train_running_loss += loss.item() / len(loader)

    return train_running_loss


def calculate_loss_and_accuracy(model, dataset, device=None, criterion=None):
    """損失・正解率を計算"""
    # 評価モードに設定
    model.eval()

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

    loss = 0.0
    total = 0
    correct = 0

    with torch.no_grad():
        for dataloader_x, dataloader_y in dataloader:
            # 順伝播
            outputs = model(dataloader_x)

            # 損失計算
            loss += criterion(outputs, dataloader_y).item()

            # 正解率計算
            pred = torch.argmax(outputs, dim=-1)
            total += len(dataloader_x)
            correct += (pred == dataloader_y).sum().item()

    return loss / len(dataset), correct / total


def padding(id_seq: str, max_len: int):
    """id_seqについて、
    max_lenより長い場合はmax_lenまでの長さにする。
    max_lenより短い場合はmax_lenになるように0を追加する。
    """
    id_list = id_seq.split(" ")
    if len(id_list) > max_len:
        id_list = id_list[:max_len]
    else:
        pad_num = max_len - len(id_list)
        for _ in range(pad_num):
            id_list.append("0")
    return " ".join(id_list)


def make_graph(value_dict: dict, value_name: str, method: str) -> None:
    """value_dictに関するgraphを生成し、保存する。"""
    for phase in ["train", "test"]:
        plt.plot(value_dict[phase], label=phase)
    plt.xlabel("epoch")
    plt.ylabel(value_name)
    plt.title(f"{value_name} per epoch")
    plt.legend()
    plt.savefig(f"{method}_{value_name}.png")
    plt.close()


if __name__ == "__main__":

    # データの読み込み
    train = pd.read_pickle("../80/train_title_id.pkl")
    test = pd.read_pickle("../80/test_title_id.pkl")
    test = test.reset_index(drop=True)

    # paddingの実施
    max_len = 10
    train["TITLE"] = train["TITLE"].apply(lambda x: padding(x, max_len))
    test["TITLE"] = test["TITLE"].apply(lambda x: padding(x, max_len))

    # yの変換
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    train["CATEGORY"] = train["CATEGORY"].map(cat_id_dict)
    test["CATEGORY"] = test["CATEGORY"].map(cat_id_dict)

    # 辞書の読み込み
    with open("../80/word_id_dict.pkl", "rb") as tf:
        word_id_dict = pickle.load(tf)

    N_LETTERS = len(word_id_dict.keys()) + 1  # pad分をplusする。
    EMB_SIZE = 300
    HIDDEN_SIZE = 50
    N_CATEGORIES = 4

    # deviceの指定
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    # modelの定義
    model = RNN(
        vocab_size=N_LETTERS,
        emb_dim=EMB_SIZE,
        hidden_size=HIDDEN_SIZE,
        output_size=N_CATEGORIES,
    ).to(device)

    # criterion, optimizerの設定
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

    # datasetの定義
    dataset_train = TextDataset(train["TITLE"], train["CATEGORY"])
    dataset_test = TextDataset(test["TITLE"], test["CATEGORY"])

    # parameterの更新
    BATCHSIZE = 32
    dataloader_train = DataLoader(
        dataset_train, batch_size=BATCHSIZE, shuffle=True, drop_last=True
    )

    train_losses = []
    train_accs = []
    test_losses = []
    test_accs = []

    EPOCH = 10
    for epoch in tqdm(range(EPOCH)):

        # 学習
        train_running_loss = train_fn(
            model,
            dataloader_train,
            device,
            optimizer,
            criterion,
            BATCHSIZE,
            HIDDEN_SIZE,
        )
        print(train_running_loss)

        # 損失と正解率の算出
        train_loss, train_acc = calculate_loss_and_accuracy(
            model, dataset_train, device, criterion
        )
        test_loss, test_acc = calculate_loss_and_accuracy(
            model, dataset_test, device, criterion
        )

        train_losses.append(train_loss)
        train_accs.append(train_acc)

        test_losses.append(test_loss)
        test_accs.append(test_acc)

        # 20epoch毎にチェックポイントを生成
        if epoch % 20 == 0:
            torch.save(model.state_dict(), f"83_model_epoch{epoch}.pth")
            torch.save(
                optimizer.state_dict(),
                f"83_optimizer_epoch{epoch}.pth",
            )

    # グラフへのプロット
    losses = {"train": train_losses, "test": test_losses}

    accs = {"train": train_accs, "test": test_accs}

    make_graph(losses, "losses", method="rnn")
    make_graph(accs, "accs", method="rnn")

    print(f"train_acc: {train_acc}")
    print(f"test_acc: {test_acc}")

グラフはこんな感じ f:id:sinchir0:20210909081556p:plain

時間: 35.65s

batchsizeが1の時に必要な時間時間:390.43sと比べると、短時間で学習が終了しているのが分かります。

84. 単語ベクトルの導入

これはちょっと注意が必要だなと感じた問題です。

nn.embedding.from_pretrainで単語埋め込みを初期化するのですが、 その際、今回使用している語彙と初期化に使用する語彙(今回はGoogle Newsデータセット)のidxを合わせる必要があります。

これをやらないと、例えばgoogle newsのidx1番目の語彙がdogで、今回のTEXTの語彙の1番目がupdateだった場合、dogの分散表現でupdateのweightを初期化してしまい、意味のない初期化となります。注意。 コードとしては下記部分でidxを合わせています。

for word, idx in word_id_dict.items():
    if word in model.vocab.keys():
        weight[idx] = torch.tensor(model[word])

以下、全体コード。

# 84. 単語ベクトルの導入
# 事前学習済みの単語ベクトル(例えば,Google Newsデータセット(約1,000億単語)での学習済み単語ベクトル)で単語埋め込みemb(x)を初期化し,学習せよ.

import os
import pickle
import random

import gensim
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from numpy.lib.function_base import kaiser
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def seed_everything(seed=42, use_torch=False):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    if use_torch:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True


seed_everything(use_torch=True)


class TextDataset(Dataset):
    def __init__(self, X, y, device):
        self.X = X
        self.y = y
        self.device = device

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        # Xをintのlistに変換
        X_list = [int(x) for x in self.X[idx].split()]

        # tensorに変換
        inputs = torch.tensor(X_list, device=self.device)
        label = torch.tensor(self.y[idx], device=self.device)

        return inputs, label


class RNN(nn.Module):
    def __init__(self, vocab_size, emb_dim, hidden_size, output_size, word_id_dict):
        super().__init__()

        # embedding層の初期化
        model = gensim.models.KeyedVectors.load_word2vec_format(
            "../../ch07/60/GoogleNews-vectors-negative300.bin", binary=True
        )
        weight = torch.zeros(len(word_id_dict) + 1, 300)  # 1はPADの分
        for word, idx in word_id_dict.items():
            if word in model.vocab.keys():
                weight[idx] = torch.tensor(model[word])

        # random
        # weight = torch.rand(len(word_id_dict)+1, 300) #ランダムな単語ベクトルでも効果があるか

        # pretrainする場合のemb
        # vocab_size, emb_dimは、GoogleNews-vectors-negative300が3000000語彙で300次元だから指定しなくて良い
        self.emb = nn.Embedding.from_pretrained(
            weight, padding_idx=0  # 0に変換された文字にベクトルを計算しない
        )

        # Use False Idx
        # model = gensim.models.KeyedVectors.load_word2vec_format('../../ch07/60/GoogleNews-vectors-negative300.bin', binary=True)
        # weights = torch.FloatTensor(model.vectors)
        # self.emb = nn.Embedding.from_pretrained(weights)

        # default
        # self.emb = nn.Embedding(
        #     vocab_size,
        #     emb_dim,
        #     padding_idx=0 # 0に変換された文字にベクトルを計算しない
        #     )

        self.rnn = nn.RNN(
            input_size=emb_dim,
            hidden_size=hidden_size,
            num_layers=1,
            nonlinearity="tanh",
            bias=True,
            batch_first=True,
        )
        # batch_first=Trueにすると、inputとoutputの型が変わる

        # inputの想定の型を、(L, N, H_in)から(N, L, H_in)に変更する
        # L: Sequence Length
        # N: Batch Size
        # H_in: input size

        # outputの想定の型を、(L, N, D * H_out)から(N, L, D * H_out)に変更する
        # L: Sequence Length
        # N: Batch Size
        # D: 2 if bidirectional=True otherwise 1
        # H_out: hidden size

        self.fc = nn.Linear(
            in_features=hidden_size, out_features=output_size, bias=True
        )

        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, h_0=None):
        x = self.emb(x)
        # 普通のとき、torch.Size([32, 10, 300])
        # 落ちるとき, torch.Size([16, 10, 300])
        # train.shape[0] == 10672
        # 10672を32で割ると、333 余り16
        # よって余りをどう扱うのかの問題になる
        # loaderの引数にdrop_last=Trueを追加すると、余りは捨てるようになる。
        x, h_t = self.rnn(x, h_0)
        x = x[:, -1, :]  # sequenceの長さ(現在は10)のうち、一番最後の出力に絞る、やっていいのかこれ?
        x = self.fc(x)
        x = self.softmax(x)
        return x


def train_fn(
    model, loader, device, optimizer, criterion, BATCHSIZE, HIDDEN_SIZE
) -> float:
    """model, loaderを用いて学習を行い、lossを返す"""
    # 学習モードに設定
    model.train()

    train_running_loss = 0.0

    for dataloader_x, dataloader_y in loader:
        dataloader_x.to(device)
        dataloader_y.to(device)

        optimizer.zero_grad()

        dataloader_y_pred_prob = model(
            x=dataloader_x,
            h_0=torch.zeros(1 * 1, BATCHSIZE, HIDDEN_SIZE),  # .to(device)
        )

        # dataloader_xでの損失の計算/
        loss = criterion(dataloader_y_pred_prob, dataloader_y)
        # 勾配の計算
        loss.backward()
        optimizer.step()

        # 訓練データでの損失の平均を計算する
        train_running_loss += loss.item() / len(loader)

    return train_running_loss


def calculate_loss_and_accuracy(model, dataset, device=None, criterion=None):
    """損失・正解率を計算"""
    # 評価モードに設定
    model.eval()

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

    loss = 0.0
    total = 0
    correct = 0

    with torch.no_grad():
        for dataloader_x, dataloader_y in dataloader:
            # 順伝播
            outputs = model(dataloader_x)

            # 損失計算
            loss += criterion(outputs, dataloader_y).item()

            # 正解率計算
            pred = torch.argmax(outputs, dim=-1)
            total += len(dataloader_x)
            correct += (pred == dataloader_y).sum().item()

    return loss / len(dataset), correct / total


def padding(id_seq: str, max_len: int):
    """id_seqについて、
    max_lenより長い場合はmax_lenまでの長さにする。
    max_lenより短い場合はmax_lenになるように0を追加する。
    """
    id_list = id_seq.split(" ")
    if len(id_list) > max_len:
        id_list = id_list[:max_len]
    else:
        pad_num = max_len - len(id_list)
        for _ in range(pad_num):
            id_list.append("0")
    return " ".join(id_list)


def make_graph(value_dict: dict, value_name: str, bn: int, method: str) -> None:
    """value_dictに関するgraphを生成し、保存する。"""
    for phase in ["train", "test"]:
        plt.plot(value_dict[phase], label=phase)
    plt.xlabel("epoch")
    plt.ylabel(value_name)
    plt.title(f"{value_name} per epoch at bn{bn}")
    plt.legend()
    plt.savefig(f"{method}_{value_name}_bn{bn}.png")
    plt.close()


if __name__ == "__main__":

    # Colab
    # PATH = '/content/drive/MyDrive/NLP100/ch09'
    # local
    PATH = ".."

    # データの読み込み
    train = pd.read_csv(f"{PATH}/80/train_title_id.csv")
    test = pd.read_csv(f"{PATH}/80/test_title_id.csv")
    test = test.reset_index(drop=True)

    # paddingの実施
    max_len = 10
    train["TITLE"] = train["TITLE"].apply(lambda x: padding(x, max_len))
    test["TITLE"] = test["TITLE"].apply(lambda x: padding(x, max_len))

    # yの変換
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    train["CATEGORY"] = train["CATEGORY"].map(cat_id_dict)
    test["CATEGORY"] = test["CATEGORY"].map(cat_id_dict)

    # 辞書の読み込み
    with open(f"{PATH}/80/word_id_dict.pkl", "rb") as tf:
        word_id_dict = pickle.load(tf)

    N_LETTERS = len(word_id_dict.keys()) + 1  # pad分をplusする。
    EMB_SIZE = 300
    HIDDEN_SIZE = 50
    N_CATEGORIES = 4

    # deviceの指定
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print(f"Use {device}")

    # modelの定義
    model = RNN(
        vocab_size=N_LETTERS,
        emb_dim=EMB_SIZE,
        hidden_size=HIDDEN_SIZE,
        output_size=N_CATEGORIES,
        word_id_dict=word_id_dict,
    ).to(device)

    # criterion, optimizerの設定
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

    # datasetの定義
    dataset_train = TextDataset(train["TITLE"], train["CATEGORY"], device)
    dataset_test = TextDataset(test["TITLE"], test["CATEGORY"], device)

    # parameterの更新
    BATCHSIZE = 32
    dataloader_train = DataLoader(
        dataset_train, batch_size=BATCHSIZE, shuffle=True, drop_last=True
    )

    train_losses = []
    train_accs = []
    test_losses = []
    test_accs = []

    EPOCH = 10
    for epoch in tqdm(range(EPOCH)):

        # 学習
        train_running_loss = train_fn(
            model,
            dataloader_train,
            device,
            optimizer,
            criterion,
            BATCHSIZE,
            HIDDEN_SIZE,
        )
        print(train_running_loss)

        # 損失と正解率の算出
        train_loss, train_acc = calculate_loss_and_accuracy(
            model, dataset_train, device, criterion
        )
        test_loss, test_acc = calculate_loss_and_accuracy(
            model, dataset_test, device, criterion
        )

        train_losses.append(train_loss)
        train_accs.append(train_acc)

        test_losses.append(test_loss)
        test_accs.append(test_acc)

        # 20epoch毎にチェックポイントを生成
        if epoch % 20 == 0:
            torch.save(model.state_dict(), f"84_model_epoch{epoch}.pth")
            torch.save(
                optimizer.state_dict(),
                f"84_optimizer_epoch{epoch}.pth",
            )

    # グラフへのプロット
    losses = {"train": train_losses, "test": test_losses}

    accs = {"train": train_accs, "test": test_accs}

    make_graph(losses, "losses", bn=BATCHSIZE, method="rnn_pretrain")
    make_graph(accs, "accs", bn=BATCHSIZE, method="rnn_pretrain")

    print(f"train_acc: {train_acc}")
    print(f"test_acc: {test_acc}")

初期化なしとありでの精度の差は下記のような感じです。 f:id:sinchir0:20210909082833p:plain

精度向上+早めに精度がサチるのがわかります。

85. 双方向RNN・多層化

双方向については、nn.RNNbidirectionalという引数があるので、それをTrueにするだけです。 多層化については、nn.RNNnum_layersという引数があるので、そこに層の数を与えるだけです。便利。 コードは3layerのものを貼っています。

# 85. 双方向RNN・多層化

import os
import pickle
import random

import gensim
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from numpy.lib.function_base import kaiser
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def seed_everything(seed=42, use_torch=False):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    if use_torch:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True


seed_everything(use_torch=True)


class TextDataset(Dataset):
    def __init__(self, X, y, device):
        self.X = X
        self.y = y
        self.device = device

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        # Xをintのlistに変換
        X_list = [int(x) for x in self.X[idx].split()]

        # tensorに変換
        inputs = torch.tensor(X_list, device=self.device)
        label = torch.tensor(self.y[idx], device=self.device)

        return inputs, label


class RNN(nn.Module):
    def __init__(self, vocab_size, emb_dim, hidden_size, output_size, word_id_dict):
        super().__init__()

        # embedding層の初期化
        model = gensim.models.KeyedVectors.load_word2vec_format(
            "../../ch07/60/GoogleNews-vectors-negative300.bin", binary=True
        )
        weight = torch.zeros(len(word_id_dict) + 1, 300)  # 1はPADの分
        for word, idx in word_id_dict.items():
            if word in model.vocab.keys():
                weight[idx] = torch.tensor(model[word])

        # pretrainする場合のemb
        # vocab_size, emb_dimは、GoogleNews-vectors-negative300が3000000語彙で300次元だから指定しなくて良い
        self.emb = nn.Embedding.from_pretrained(
            weight, padding_idx=0  # 0に変換された文字にベクトルを計算しない
        )

        # random
        # weight = torch.rand(len(word_id_dict)+1, 300) #ランダムな単語ベクトルでも効果があるか
        # self.emb = nn.Embedding.from_pretrained(weights)

        # Use False Idx
        # model = gensim.models.KeyedVectors.load_word2vec_format('../../ch07/60/GoogleNews-vectors-negative300.bin', binary=True)
        # weights = torch.FloatTensor(model.vectors)
        # self.emb = nn.Embedding.from_pretrained(weights)

        # # default
        # self.emb = nn.Embedding(
        #     vocab_size,
        #     emb_dim,
        #     padding_idx=0 # 0に変換された文字にベクトルを計算しない
        #     )

        self.rnn = nn.RNN(
            input_size=emb_dim,
            hidden_size=hidden_size,
            num_layers=3,
            nonlinearity="tanh",
            bias=True,
            batch_first=True,
            bidirectional=True,
        )
        # batch_first=Trueにすると、inputとoutputの型が変わる

        # inputの想定の型を、(L, N, H_in)から(N, L, H_in)に変更する
        # L: Sequence Length
        # N: Batch Size
        # H_in: input size

        # outputの想定の型を、(L, N, D * H_out)から(N, L, D * H_out)に変更する
        # L: Sequence Length
        # N: Batch Size
        # D: 2 if bidirectional=True otherwise 1
        # H_out: hidden size

        self.fc = nn.Linear(
            in_features=hidden_size * 2, out_features=output_size, bias=True
        )

        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, h_0=None):
        x = self.emb(x)
        # 普通のとき、torch.Size([32, 10, 300])
        # 落ちるとき, torch.Size([16, 10, 300])
        # train.shape[0] == 10672
        # 10672を32で割ると、333 余り16
        # よって余りをどう扱うのかの問題になる
        # loaderの引数にdrop_last=Trueを追加すると、余りは捨てるようになる。
        x, h_t = self.rnn(x, h_0)
        x = x[:, -1, :]  # sequenceの長さ(現在は10)のうち、一番最後の出力に絞る、やっていいのかこれ?
        x = self.fc(x)
        x = self.softmax(x)
        return x


def train_fn(
    model, loader, device, optimizer, criterion, BATCHSIZE, HIDDEN_SIZE
) -> float:
    """model, loaderを用いて学習を行い、lossを返す"""
    # 学習モードに設定
    model.train()

    train_running_loss = 0.0

    for dataloader_x, dataloader_y in loader:
        dataloader_x.to(device)
        dataloader_y.to(device)

        optimizer.zero_grad()

        dataloader_y_pred_prob = model(
            x=dataloader_x,
            h_0=torch.zeros(2 * 3, BATCHSIZE, HIDDEN_SIZE).to(
                device
            ),  # D * num_layer, bidirectionnalの場合はD=2
        )

        # dataloader_xでの損失の計算/
        loss = criterion(dataloader_y_pred_prob, dataloader_y)
        # 勾配の計算
        loss.backward()
        optimizer.step()

        # 訓練データでの損失の平均を計算する
        train_running_loss += loss.item() / len(loader)

    return train_running_loss


def calculate_loss_and_accuracy(model, dataset, device=None, criterion=None):
    """損失・正解率を計算"""
    # 評価モードに設定
    model.eval()

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

    loss = 0.0
    total = 0
    correct = 0

    with torch.no_grad():
        for dataloader_x, dataloader_y in dataloader:
            # 順伝播
            outputs = model(dataloader_x)

            # 損失計算
            loss += criterion(outputs, dataloader_y).item()

            # 正解率計算
            pred = torch.argmax(outputs, dim=-1)
            total += len(dataloader_x)
            correct += (pred == dataloader_y).sum().item()

    return loss / len(dataset), correct / total


def padding(id_seq: str, max_len: int):
    """id_seqについて、
    max_lenより長い場合はmax_lenまでの長さにする。
    max_lenより短い場合はmax_lenになるように0を追加する。
    """
    id_list = id_seq.split(" ")
    if len(id_list) > max_len:
        id_list = id_list[:max_len]
    else:
        pad_num = max_len - len(id_list)
        for _ in range(pad_num):
            id_list.append("0")
    return " ".join(id_list)


def make_graph(value_dict: dict, value_name: str, bn: int, method: str) -> None:
    """value_dictに関するgraphを生成し、保存する。"""
    for phase in ["train", "test"]:
        plt.plot(value_dict[phase], label=phase)
    plt.xlabel("epoch")
    plt.ylabel(value_name)
    plt.title(f"{value_name} per epoch at bn{bn}")
    plt.legend()
    plt.savefig(f"{method}_{value_name}_bn{bn}.png")
    plt.close()


if __name__ == "__main__":

    # Colab
    # PATH = '/content/drive/MyDrive/NLP100/ch09'
    # local
    PATH = ".."

    # データの読み込み
    train = pd.read_csv(f"{PATH}/80/train_title_id.csv")
    test = pd.read_csv(f"{PATH}/80/test_title_id.csv")
    test = test.reset_index(drop=True)

    # paddingの実施
    max_len = 10
    train["TITLE"] = train["TITLE"].apply(lambda x: padding(x, max_len))
    test["TITLE"] = test["TITLE"].apply(lambda x: padding(x, max_len))

    # yの変換
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    train["CATEGORY"] = train["CATEGORY"].map(cat_id_dict)
    test["CATEGORY"] = test["CATEGORY"].map(cat_id_dict)

    # 辞書の読み込み
    with open(f"{PATH}/80/word_id_dict.pkl", "rb") as tf:
        word_id_dict = pickle.load(tf)

    N_LETTERS = len(word_id_dict.keys()) + 1  # pad分をplusする。
    EMB_SIZE = 300
    HIDDEN_SIZE = 50
    N_CATEGORIES = 4

    # deviceの指定
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print(f"Use {device}")

    # modelの定義
    model = RNN(
        vocab_size=N_LETTERS,
        emb_dim=EMB_SIZE,
        hidden_size=HIDDEN_SIZE,
        output_size=N_CATEGORIES,
        word_id_dict=word_id_dict,
    ).to(device)

    # criterion, optimizerの設定
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

    # datasetの定義
    dataset_train = TextDataset(train["TITLE"], train["CATEGORY"], device)
    dataset_test = TextDataset(test["TITLE"], test["CATEGORY"], device)

    # parameterの更新
    BATCHSIZE = 32
    dataloader_train = DataLoader(
        dataset_train, batch_size=BATCHSIZE, shuffle=True, drop_last=True
    )

    train_losses = []
    train_accs = []
    test_losses = []
    test_accs = []

    EPOCH = 10
    for epoch in tqdm(range(EPOCH)):

        # 学習
        train_running_loss = train_fn(
            model,
            dataloader_train,
            device,
            optimizer,
            criterion,
            BATCHSIZE,
            HIDDEN_SIZE,
        )
        print(train_running_loss)

        # 損失と正解率の算出
        train_loss, train_acc = calculate_loss_and_accuracy(
            model, dataset_train, device, criterion
        )
        test_loss, test_acc = calculate_loss_and_accuracy(
            model, dataset_test, device, criterion
        )

        train_losses.append(train_loss)
        train_accs.append(train_acc)

        test_losses.append(test_loss)
        test_accs.append(test_acc)

        # 20epoch毎にチェックポイントを生成
        if epoch % 20 == 0:
            torch.save(model.state_dict(), f"85_model_epoch{epoch}.pth")
            torch.save(
                optimizer.state_dict(),
                f"85_optimizer_epoch{epoch}.pth",
            )

    # グラフへのプロット
    losses = {"train": train_losses, "test": test_losses}

    accs = {"train": train_accs, "test": test_accs}

    make_graph(losses, "losses", bn=BATCHSIZE, method="rnn_bidirectional_3layer")
    make_graph(accs, "accs", bn=BATCHSIZE, method="rnn_bidirectional_3layer")

    print(f"train_acc: {train_acc: .4f}")
    print(f"test_acc: {test_acc: .4f}")

グラフはこちら、num_layersを増やすと精度的には悪化していくのは違和感ですね。 f:id:sinchir0:20210910074912p:plain

86. 畳み込みニューラルネットワーク (CNN)

nn.RNNで構築していた部分はnn.Conv1dnn.MaxPool1dで置き換えてあげます。 どの次元が何を表すかは、公式リファレンスを見ながら注意して構築していきました。 例えば、'nn.Conv1d'は下記のような感じです。

input size: (N, C_in, L)
N=batch size
C_m=number of channels
L=length of signal sequence
output size: (N, C_out, L_out)

実際のコードはこちら

# 86. 畳み込みニューラルネットワーク (CNN)

import os
import pickle
import random

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from numpy.lib.function_base import kaiser
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def seed_everything(seed=42, use_torch=False):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    if use_torch:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True


seed_everything(use_torch=True)


class TextDataset(Dataset):
    def __init__(self, X, y, device):
        self.X = X
        self.y = y
        self.device = device

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        # Xをintのlistに変換
        X_list = [int(x) for x in self.X[idx].split()]

        # tensorに変換
        inputs = torch.tensor(X_list, device=self.device)
        label = torch.tensor(self.y[idx], device=self.device)

        return inputs, label


class CNN(nn.Module):
    def __init__(self, vocab_size, emb_dim, hidden_size, output_size, word_id_dict):
        super().__init__()

        self.emb = nn.Embedding(
            vocab_size, emb_dim, padding_idx=0  # 0に変換された文字にベクトルを計算しない
        )

        self.conv1d = nn.Conv1d(
            in_channels=emb_dim,
            out_channels=hidden_size,
            kernel_size=3,
            stride=1,
            padding=0,  # 4方向にpaddingを行う
            padding_mode="zeros",
        )
        # input size: (N, C_in, L)
        # N=batch size
        # C_m=number of channels
        # L=length of signal sequence
        # output size: (N, C_out, L_out)

        self.pool1d = nn.MaxPool1d(
            kernel_size=8  # conv1dにて300→50にすると、Sequential lengthは10→8になって返ってくる。この8を1にしたいので、全体でpooling
        )
        # input size: (N, C, L)
        # N=batch size
        # C_m=number of channels
        # L=length of signal sequence
        # output size: (N, C, L_out)

        self.fc = nn.Linear(
            in_features=hidden_size, out_features=output_size, bias=True
        )

        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, h_0=None):
        x = self.emb(x)
        x = x.permute(
            0, 2, 1
        )  # 2次元目の数値を1次元目と入れ替え、1次元目にemb dim,2次元目にSequential lengthを持ってくる
        x = self.conv1d(x)
        x = self.pool1d(x)
        x = x[:, :, -1]  # (N, C, L_out)の次元L_outをなくす
        x = self.fc(x)
        x = self.softmax(x)
        return x
        # UserWarning: Named tensors and・・・で謎Warniningが出るけど、気にしなくて良いっぽい
        # https://github.com/pytorch/pytorch/pull/60059


def train_fn(
    model, loader, device, optimizer, criterion, BATCHSIZE, HIDDEN_SIZE
) -> float:
    """model, loaderを用いて学習を行い、lossを返す"""
    # 学習モードに設定
    model.train()

    train_running_loss = 0.0

    for dataloader_x, dataloader_y in loader:
        dataloader_x.to(device)
        dataloader_y.to(device)

        # optimizer.zero_grad()

        dataloader_y_pred_prob = model(
            x=dataloader_x,
            h_0=torch.zeros(2 * 1, BATCHSIZE, HIDDEN_SIZE).to(
                device
            ),  # D * num_layer, bidirectionnalの場合はD=2
        )

        # dataloader_xでの損失の計算/
        loss = criterion(dataloader_y_pred_prob, dataloader_y)
        # 勾配の計算
        loss.backward()
        # optimizer.step()

        # 訓練データでの損失の平均を計算する
        train_running_loss += loss.item() / len(loader)

    return train_running_loss


def calculate_loss_and_accuracy(model, dataset, device=None, criterion=None):
    """損失・正解率を計算"""
    # 評価モードに設定
    model.eval()

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

    loss = 0.0
    total = 0
    correct = 0

    with torch.no_grad():
        for dataloader_x, dataloader_y in dataloader:
            # 順伝播
            outputs = model(dataloader_x)

            # 損失計算
            loss += criterion(outputs, dataloader_y).item()

            # 正解率計算
            pred = torch.argmax(outputs, dim=-1)
            total += len(dataloader_x)
            correct += (pred == dataloader_y).sum().item()

    return loss / len(dataset), correct / total


def padding(id_seq: str, max_len: int):
    """id_seqについて、
    max_lenより長い場合はmax_lenまでの長さにする。
    max_lenより短い場合はmax_lenになるように0を追加する。
    """
    id_list = id_seq.split(" ")
    if len(id_list) > max_len:
        id_list = id_list[:max_len]
    else:
        pad_num = max_len - len(id_list)
        for _ in range(pad_num):
            id_list.append("0")
    return " ".join(id_list)


def make_graph(value_dict: dict, value_name: str, bn: int, method: str) -> None:
    """value_dictに関するgraphを生成し、保存する。"""
    for phase in ["train", "test"]:
        plt.plot(value_dict[phase], label=phase)
    plt.xlabel("epoch")
    plt.ylabel(value_name)
    plt.title(f"{value_name} per epoch at bn{bn}")
    plt.legend()
    plt.savefig(f"{method}_{value_name}_bn{bn}.png")
    plt.close()


if __name__ == "__main__":

    # Colab
    # PATH = '/content/drive/MyDrive/NLP100/ch09'
    # local
    PATH = ".."

    # データの読み込み
    train = pd.read_csv(f"{PATH}/80/train_title_id.csv")
    test = pd.read_csv(f"{PATH}/80/test_title_id.csv")
    test = test.reset_index(drop=True)

    # paddingの実施
    max_len = 10
    train["TITLE"] = train["TITLE"].apply(lambda x: padding(x, max_len))
    test["TITLE"] = test["TITLE"].apply(lambda x: padding(x, max_len))

    # yの変換
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    train["CATEGORY"] = train["CATEGORY"].map(cat_id_dict)
    test["CATEGORY"] = test["CATEGORY"].map(cat_id_dict)

    # 辞書の読み込み
    with open(f"{PATH}/80/word_id_dict.pkl", "rb") as tf:
        word_id_dict = pickle.load(tf)

    N_LETTERS = len(word_id_dict.keys()) + 1  # pad分をplusする。
    EMB_SIZE = 300
    HIDDEN_SIZE = 50
    N_CATEGORIES = 4

    # deviceの指定
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print(f"Use {device}")

    # modelの定義
    model = CNN(
        vocab_size=N_LETTERS,
        emb_dim=EMB_SIZE,
        hidden_size=HIDDEN_SIZE,
        output_size=N_CATEGORIES,
        word_id_dict=word_id_dict,
    ).to(device)

    # criterion, optimizerの設定
    criterion = nn.CrossEntropyLoss()
    # optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
    optimizer = None

    # datasetの定義
    dataset_train = TextDataset(train["TITLE"], train["CATEGORY"], device)
    dataset_test = TextDataset(test["TITLE"], test["CATEGORY"], device)

    # parameterの更新
    BATCHSIZE = 32
    dataloader_train = DataLoader(
        dataset_train, batch_size=BATCHSIZE, shuffle=True, drop_last=True
    )

    train_losses = []
    train_accs = []
    test_losses = []
    test_accs = []

    EPOCH = 10
    for epoch in tqdm(range(EPOCH)):

        # 学習
        train_running_loss = train_fn(
            model,
            dataloader_train,
            device,
            optimizer,
            criterion,
            BATCHSIZE,
            HIDDEN_SIZE,
        )
        print(train_running_loss)

        # 損失と正解率の算出
        train_loss, train_acc = calculate_loss_and_accuracy(
            model, dataset_train, device, criterion
        )
        test_loss, test_acc = calculate_loss_and_accuracy(
            model, dataset_test, device, criterion
        )

        train_losses.append(train_loss)
        train_accs.append(train_acc)

        test_losses.append(test_loss)
        test_accs.append(test_acc)

        # 20epoch毎にチェックポイントを生成
        if epoch % 20 == 0:
            torch.save(model.state_dict(), f"86_model_epoch{epoch}.pth")
            # torch.save(
            #     optimizer.state_dict(),
            #     f"86_optimizer_epoch{epoch}.pth",
            # )

    # グラフへのプロット
    losses = {"train": train_losses, "test": test_losses}

    accs = {"train": train_accs, "test": test_accs}

    make_graph(losses, "losses", bn=BATCHSIZE, method="cnn")
    make_graph(accs, "accs", bn=BATCHSIZE, method="cnn")

    print(f"train_acc: {train_acc: .4f}")
    print(f"test_acc: {test_acc: .4f}")

87. 確率的勾配降下法によるCNNの学習

SGDを追加して学習します。

# 87. 確率的勾配降下法によるCNNの学習

import os
import pickle
import random

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from numpy.lib.function_base import kaiser
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def seed_everything(seed=42, use_torch=False):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    if use_torch:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True


seed_everything(use_torch=True)


class TextDataset(Dataset):
    def __init__(self, X, y, device):
        self.X = X
        self.y = y
        self.device = device

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        # Xをintのlistに変換
        X_list = [int(x) for x in self.X[idx].split()]

        # tensorに変換
        inputs = torch.tensor(X_list, device=self.device)
        label = torch.tensor(self.y[idx], device=self.device)

        return inputs, label


class CNN(nn.Module):
    def __init__(self, vocab_size, emb_dim, hidden_size, output_size, word_id_dict):
        super().__init__()

        # # embedding層の初期化
        # model = gensim.models.KeyedVectors.load_word2vec_format('../../ch07/60/GoogleNews-vectors-negative300.bin', binary=True)
        # weight = torch.zeros(len(word_id_dict)+1, 300) # 1はPADの分
        # for word, idx in word_id_dict.items():
        #     if word in model.vocab.keys():
        #         weight[idx] = torch.tensor(model[word])

        # # pretrainする場合のemb
        # # vocab_size, emb_dimは、GoogleNews-vectors-negative300が3000000語彙で300次元だから指定しなくて良い
        # self.emb = nn.Embedding.from_pretrained(
        #     weight,
        #     padding_idx=0 # 0に変換された文字にベクトルを計算しない
        #     )

        # default
        self.emb = nn.Embedding(
            vocab_size, emb_dim, padding_idx=0  # 0に変換された文字にベクトルを計算しない
        )

        # self.rnn = nn.RNN(
        #     input_size=emb_dim,
        #     hidden_size=hidden_size,
        #     num_layers=1,
        #     nonlinearity='tanh',
        #     bias=True,
        #     batch_first=True,
        #     bidirectional=True
        #     )

        self.conv1d = nn.Conv1d(
            in_channels=emb_dim,
            out_channels=hidden_size,
            kernel_size=3,
            stride=1,
            padding=0,  # 4方向にpaddingを行う
            padding_mode="zeros",
        )
        # input size: (N, C_in, L)
        # N=batch size
        # C_m=number of channels
        # L=length of signal sequence
        # output size: (N, C_out, L_out)

        self.pool1d = nn.MaxPool1d(
            kernel_size=8  # conv1dにて300→50にすると、Sequential lengthは10→8になって返ってくる。この8を1にしたいので、全体でpooling
        )
        # input size: (N, C, L)
        # N=batch size
        # C_m=number of channels
        # L=length of signal sequence
        # output size: (N, C, L_out)

        self.fc = nn.Linear(
            in_features=hidden_size, out_features=output_size, bias=True
        )

        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, h_0=None):
        x = self.emb(x)
        x = x.permute(
            0, 2, 1
        )  # 2次元目の数値を1次元目と入れ替え、1次元目にemb dim,2次元目にSequential lengthを持ってくる
        x = self.conv1d(x)
        x = self.pool1d(x)
        x = x[:, :, -1]  # (N, C, L_out)の次元L_outをなくす
        x = self.fc(x)
        x = self.softmax(x)
        return x
        # UserWarning: Named tensors and・・・で謎Warniningが出るけど、気にしなくて良いっぽい
        # https://github.com/pytorch/pytorch/pull/60059


def train_fn(
    model, loader, device, optimizer, criterion, BATCHSIZE, HIDDEN_SIZE
) -> float:
    """model, loaderを用いて学習を行い、lossを返す"""
    # 学習モードに設定
    model.train()

    train_running_loss = 0.0

    for dataloader_x, dataloader_y in loader:
        dataloader_x.to(device)
        dataloader_y.to(device)

        optimizer.zero_grad()

        dataloader_y_pred_prob = model(
            x=dataloader_x,
            h_0=torch.zeros(2 * 1, BATCHSIZE, HIDDEN_SIZE).to(
                device
            ),  # D * num_layer, bidirectionnalの場合はD=2
        )

        # dataloader_xでの損失の計算/
        loss = criterion(dataloader_y_pred_prob, dataloader_y)
        # 勾配の計算
        loss.backward()
        optimizer.step()

        # 訓練データでの損失の平均を計算する
        train_running_loss += loss.item() / len(loader)

    return train_running_loss


def calculate_loss_and_accuracy(model, dataset, device=None, criterion=None):
    """損失・正解率を計算"""
    # 評価モードに設定
    model.eval()

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

    loss = 0.0
    total = 0
    correct = 0

    with torch.no_grad():
        for dataloader_x, dataloader_y in dataloader:
            # 順伝播
            outputs = model(dataloader_x)

            # 損失計算
            loss += criterion(outputs, dataloader_y).item()

            # 正解率計算
            pred = torch.argmax(outputs, dim=-1)
            total += len(dataloader_x)
            correct += (pred == dataloader_y).sum().item()

    return loss / len(dataset), correct / total


def padding(id_seq: str, max_len: int):
    """id_seqについて、
    max_lenより長い場合はmax_lenまでの長さにする。
    max_lenより短い場合はmax_lenになるように0を追加する。
    """
    id_list = id_seq.split(" ")
    if len(id_list) > max_len:
        id_list = id_list[:max_len]
    else:
        pad_num = max_len - len(id_list)
        for _ in range(pad_num):
            id_list.append("0")
    return " ".join(id_list)


def make_graph(value_dict: dict, value_name: str, bn: int, method: str) -> None:
    """value_dictに関するgraphを生成し、保存する。"""
    for phase in ["train", "test"]:
        plt.plot(value_dict[phase], label=phase)
    plt.xlabel("epoch")
    plt.ylabel(value_name)
    plt.title(f"{value_name} per epoch at bn{bn}")
    plt.legend()
    plt.savefig(f"{method}_{value_name}_bn{bn}.png")
    plt.close()


if __name__ == "__main__":

    # Colab
    # PATH = '/content/drive/MyDrive/NLP100/ch09'
    # local
    PATH = ".."

    # データの読み込み
    train = pd.read_csv(f"{PATH}/80/train_title_id.csv")
    test = pd.read_csv(f"{PATH}/80/test_title_id.csv")
    test = test.reset_index(drop=True)

    # paddingの実施
    max_len = 10
    train["TITLE"] = train["TITLE"].apply(lambda x: padding(x, max_len))
    test["TITLE"] = test["TITLE"].apply(lambda x: padding(x, max_len))

    # yの変換
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    train["CATEGORY"] = train["CATEGORY"].map(cat_id_dict)
    test["CATEGORY"] = test["CATEGORY"].map(cat_id_dict)

    # 辞書の読み込み
    with open(f"{PATH}/80/word_id_dict.pkl", "rb") as tf:
        word_id_dict = pickle.load(tf)

    N_LETTERS = len(word_id_dict.keys()) + 1  # pad分をplusする。
    EMB_SIZE = 300
    HIDDEN_SIZE = 50
    N_CATEGORIES = 4

    # deviceの指定
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print(f"Use {device}")

    # modelの定義
    model = CNN(
        vocab_size=N_LETTERS,
        emb_dim=EMB_SIZE,
        hidden_size=HIDDEN_SIZE,
        output_size=N_CATEGORIES,
        word_id_dict=word_id_dict,
    ).to(device)

    # criterion, optimizerの設定
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

    # datasetの定義
    dataset_train = TextDataset(train["TITLE"], train["CATEGORY"], device)
    dataset_test = TextDataset(test["TITLE"], test["CATEGORY"], device)

    # parameterの更新
    BATCHSIZE = 32
    dataloader_train = DataLoader(
        dataset_train, batch_size=BATCHSIZE, shuffle=True, drop_last=True
    )

    train_losses = []
    train_accs = []
    test_losses = []
    test_accs = []

    EPOCH = 10
    for epoch in tqdm(range(EPOCH)):

        # 学習
        train_running_loss = train_fn(
            model,
            dataloader_train,
            device,
            optimizer,
            criterion,
            BATCHSIZE,
            HIDDEN_SIZE,
        )
        print(train_running_loss)

        # 損失と正解率の算出
        train_loss, train_acc = calculate_loss_and_accuracy(
            model, dataset_train, device, criterion
        )
        test_loss, test_acc = calculate_loss_and_accuracy(
            model, dataset_test, device, criterion
        )

        train_losses.append(train_loss)
        train_accs.append(train_acc)

        test_losses.append(test_loss)
        test_accs.append(test_acc)

        # 20epoch毎にチェックポイントを生成
        if epoch % 20 == 0:
            torch.save(model.state_dict(), f"87_model_epoch{epoch}.pth")
            torch.save(
                optimizer.state_dict(),
                f"87_optimizer_epoch{epoch}.pth",
            )

    # グラフへのプロット
    losses = {"train": train_losses, "test": test_losses}

    accs = {"train": train_accs, "test": test_accs}

    make_graph(losses, "losses", bn=BATCHSIZE, method="cnn")
    make_graph(accs, "accs", bn=BATCHSIZE, method="cnn")

    print(f"train_acc: {train_acc: .4f}")
    print(f"test_acc: {test_acc: .4f}")

グラフはこちら。RNNよりもtrainとtestの乖離が酷くなりました。 f:id:sinchir0:20210910075921p:plain

88. パラメータチューニング

ハイパラチューニングの域を超えていますが、rnn→bi-lstmに変更し、epochを30までに変更しました。

# 88. パラメータチューニング

import os
import pickle
import random

import gensim
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from numpy.lib.function_base import kaiser
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm


def seed_everything(seed=42, use_torch=False):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    if use_torch:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True


seed_everything(use_torch=True)


class TextDataset(Dataset):
    def __init__(self, X, y, device):
        self.X = X
        self.y = y
        self.device = device

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        # Xをintのlistに変換
        X_list = [int(x) for x in self.X[idx].split()]

        # tensorに変換
        inputs = torch.tensor(X_list, device=self.device)
        label = torch.tensor(self.y[idx], device=self.device)

        return inputs, label


class RNN(nn.Module):
    def __init__(self, vocab_size, emb_dim, hidden_size, output_size, word_id_dict):
        super().__init__()

        # embedding層の初期化
        model = gensim.models.KeyedVectors.load_word2vec_format(
            "../../ch07/60/GoogleNews-vectors-negative300.bin", binary=True
        )
        weight = torch.zeros(len(word_id_dict) + 1, 300)  # 1はPADの分
        for word, idx in word_id_dict.items():
            if word in model.vocab.keys():
                weight[idx] = torch.tensor(model[word])

        # pretrainする場合のemb
        # vocab_size, emb_dimは、GoogleNews-vectors-negative300が3000000語彙で300次元だから指定しなくて良い
        self.emb = nn.Embedding.from_pretrained(
            weight, padding_idx=0  # 0に変換された文字にベクトルを計算しない
        )

        self.lstm = nn.LSTM(
            input_size=emb_dim,
            hidden_size=hidden_size,
            num_layers=1,
            bias=True,
            batch_first=True,
            dropout=0.25,
            bidirectional=True,
        )
        # batch_first=Trueにすると、inputとoutputの型が変わる

        # inputの想定の型を、(L, N, H_in)から(N, L, H_in)に変更する
        # L: Sequence Length
        # N: Batch Size
        # H_in: input size

        # outputの想定の型を、(L, N, D * H_out)から(N, L, D * H_out)に変更する
        # L: Sequence Length
        # N: Batch Size
        # D: 2 if bidirectional=True otherwise 1
        # H_out: hidden size

        self.fc = nn.Linear(
            in_features=hidden_size * 2, out_features=output_size, bias=True
        )

        self.softmax = nn.Softmax(dim=1)

    def forward(self, x, h_0=None):
        x = self.emb(x)
        # 普通のとき、torch.Size([32, 10, 300])
        # 落ちるとき, torch.Size([16, 10, 300])
        # train.shape[0] == 10672
        # 10672を32で割ると、333 余り16
        # よって余りをどう扱うのかの問題になる
        # loaderの引数にdrop_last=Trueを追加すると、余りは捨てるようになる。
        x, h_t = self.lstm(x, h_0)
        x = x[:, -1, :]
        x = self.fc(x)
        x = self.softmax(x)
        return x


def train_fn(
    model, loader, device, optimizer, criterion, BATCHSIZE, HIDDEN_SIZE
) -> float:
    """model, loaderを用いて学習を行い、lossを返す"""
    # 学習モードに設定
    model.train()

    train_running_loss = 0.0

    for dataloader_x, dataloader_y in loader:
        dataloader_x.to(device)
        dataloader_y.to(device)

        optimizer.zero_grad()

        dataloader_y_pred_prob = model(
            x=dataloader_x,
            # h_0=torch.zeros(2 * 1, BATCHSIZE, HIDDEN_SIZE).to(device) # D * num_layer, bidirectionnalの場合はD=2
            h_0=None,
        )

        # dataloader_xでの損失の計算/
        loss = criterion(dataloader_y_pred_prob, dataloader_y)
        # 勾配の計算
        loss.backward()
        optimizer.step()

        # 訓練データでの損失の平均を計算する
        train_running_loss += loss.item() / len(loader)

    return train_running_loss


def calculate_loss_and_accuracy(model, dataset, device=None, criterion=None):
    """損失・正解率を計算"""
    # 評価モードに設定
    model.eval()

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

    loss = 0.0
    total = 0
    correct = 0

    with torch.no_grad():
        for dataloader_x, dataloader_y in dataloader:
            # 順伝播
            outputs = model(dataloader_x)

            # 損失計算
            loss += criterion(outputs, dataloader_y).item()

            # 正解率計算
            pred = torch.argmax(outputs, dim=-1)
            total += len(dataloader_x)
            correct += (pred == dataloader_y).sum().item()

    return loss / len(dataset), correct / total


def padding(id_seq: str, max_len: int):
    """id_seqについて、
    max_lenより長い場合はmax_lenまでの長さにする。
    max_lenより短い場合はmax_lenになるように0を追加する。
    """
    id_list = id_seq.split(" ")
    if len(id_list) > max_len:
        id_list = id_list[:max_len]
    else:
        pad_num = max_len - len(id_list)
        for _ in range(pad_num):
            id_list.append("0")
    return " ".join(id_list)


def make_graph(value_dict: dict, value_name: str, bn: int, method: str) -> None:
    """value_dictに関するgraphを生成し、保存する。"""
    for phase in ["train", "test"]:
        plt.plot(value_dict[phase], label=phase)
    plt.xlabel("epoch")
    plt.ylabel(value_name)
    plt.title(f"{value_name} per epoch at bn{bn}")
    plt.legend()
    plt.savefig(f"{method}_{value_name}_bn{bn}.png")
    plt.close()


if __name__ == "__main__":

    # Colab
    # PATH = '/content/drive/MyDrive/NLP100/ch09'
    # local
    PATH = ".."

    # データの読み込み
    train = pd.read_csv(f"{PATH}/80/train_title_id.csv")
    test = pd.read_csv(f"{PATH}/80/test_title_id.csv")
    test = test.reset_index(drop=True)

    # paddingの実施
    max_len = 10
    train["TITLE"] = train["TITLE"].apply(lambda x: padding(x, max_len))
    test["TITLE"] = test["TITLE"].apply(lambda x: padding(x, max_len))

    # yの変換
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    train["CATEGORY"] = train["CATEGORY"].map(cat_id_dict)
    test["CATEGORY"] = test["CATEGORY"].map(cat_id_dict)

    # 辞書の読み込み
    with open(f"{PATH}/80/word_id_dict.pkl", "rb") as tf:
        word_id_dict = pickle.load(tf)

    N_LETTERS = len(word_id_dict.keys()) + 1  # pad分をplusする。
    EMB_SIZE = 300
    HIDDEN_SIZE = 50
    N_CATEGORIES = 4

    # deviceの指定
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print(f"Use {device}")

    # modelの定義
    model = RNN(
        vocab_size=N_LETTERS,
        emb_dim=EMB_SIZE,
        hidden_size=HIDDEN_SIZE,
        output_size=N_CATEGORIES,
        word_id_dict=word_id_dict,
    ).to(device)

    # criterion, optimizerの設定
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

    # datasetの定義
    dataset_train = TextDataset(train["TITLE"], train["CATEGORY"], device)
    dataset_test = TextDataset(test["TITLE"], test["CATEGORY"], device)

    # parameterの更新
    BATCHSIZE = 32
    dataloader_train = DataLoader(
        dataset_train, batch_size=BATCHSIZE, shuffle=True, drop_last=True
    )

    train_losses = []
    train_accs = []
    test_losses = []
    test_accs = []

    EPOCH = 30
    for epoch in tqdm(range(EPOCH)):

        # 学習
        train_running_loss = train_fn(
            model,
            dataloader_train,
            device,
            optimizer,
            criterion,
            BATCHSIZE,
            HIDDEN_SIZE,
        )
        print(train_running_loss)

        # 損失と正解率の算出
        train_loss, train_acc = calculate_loss_and_accuracy(
            model, dataset_train, device, criterion
        )
        test_loss, test_acc = calculate_loss_and_accuracy(
            model, dataset_test, device, criterion
        )

        train_losses.append(train_loss)
        train_accs.append(train_acc)

        test_losses.append(test_loss)
        test_accs.append(test_acc)

        # 20epoch毎にチェックポイントを生成
        if epoch % 20 == 0:
            torch.save(model.state_dict(), f"88_model_epoch{epoch}.pth")
            torch.save(
                optimizer.state_dict(),
                f"88_optimizer_epoch{epoch}.pth",
            )

    # グラフへのプロット
    losses = {"train": train_losses, "test": test_losses}

    accs = {"train": train_accs, "test": test_accs}

    make_graph(losses, "losses", bn=BATCHSIZE, method="bilstm")
    make_graph(accs, "accs", bn=BATCHSIZE, method="bilstm")

    print(f"train_acc: {train_acc: .4f}")
    print(f"test_acc: {test_acc: .4f}")

グラフはこちら。RNNよりも精度は落ちています。 f:id:sinchir0:20210910080242p:plain

89. 事前学習済み言語モデルからの転移学習

BERT, RoBERTa, ALBERT, CANINEでやってみました。 コード例はBERTです。

# 89. 事前学習済み言語モデルからの転移学習
# 事前学習済み言語モデル(例えばBERTなど)を出発点として,ニュース記事見出しをカテゴリに分類するモデルを構築せよ.

# https://qiita.com/yamaru/items/63a342c844cff056a549

import os
import random
import time

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm
from transformers import BertModel, BertTokenizer


def seed_everything(seed=42, use_torch=False):
    random.seed(seed)
    os.environ["PYTHONHASHSEED"] = str(seed)
    np.random.seed(seed)
    if use_torch:
        torch.manual_seed(seed)
        torch.cuda.manual_seed(seed)
        torch.backends.cudnn.deterministic = True


class TextDataset(Dataset):
    def __init__(self, X, y, tokenizer, max_len, device):
        self.X = X
        self.y = y
        self.tokenizer = tokenizer
        self.max_len = max_len
        self.device = device

    def __len__(self):  # len(Dataset)で返す値を指定
        return len(self.X)

    def __getitem__(self, idx):  # Dataset[index]で返す値を指定
        text = self.X[idx]
        inputs = self.tokenizer.encode_plus(
            text,
            add_special_tokens=True,
            max_length=self.max_len,
            # pad_to_max_length=True
            padding="max_length",
            truncation=True,
        )
        ids = inputs["input_ids"]
        mask = inputs["attention_mask"]

        return {
            "ids": torch.tensor(ids, device=device),
            "mask": torch.tensor(mask, device=device),
            "labels": torch.tensor([self.y[idx]], device=device),
        }


class BERTClass(nn.Module):
    def __init__(self, drop_rate, output_size):
        super().__init__()
        # outputs = model(ids, mask)
        # にて、下のエラーで落ちる
        # TypeError: dropout(): argument 'input' (position 1) must be Tensor, not str
        # https://github.com/huggingface/transformers/issues/8879#issuecomment-796328753
        # return_dict=Falseを追加したら解決
        # self.bert = BertModel.from_pretrained('bert-base-uncased', return_dict=False)
        self.bert = BertModel.from_pretrained("bert-base-uncased")
        self.drop = nn.Dropout(drop_rate)
        self.fc = nn.Linear(768, output_size)  # BERTの出力に合わせて768次元を指定
        self.softmax = nn.Softmax(dim=1)

    def forward(self, ids, mask):
        # _, x = self.bert(ids, attention_mask=mask)
        outputs = self.bert(ids, attention_mask=mask)
        _, x = outputs["last_hidden_state"], outputs["pooler_output"]
        # 引数の一つ目は、(batch_size, seq_length=10, 768)のテンソル、last_hidden_state
        # 引数の二つめは、(batch_size, 768)のテンソル、pooler_output
        # これは先頭単語[CLS]を取り出して、BertPoolerにて、同じhiddensize→hiddensizeへと全結合層を通して、その後tanhを通して-1~1にしたもの。
        x = self.drop(x)
        x = self.fc(x)
        x = self.softmax(x)
        return x


def train_fn(model, dataset, device, optimizer, criterion, BATCH_SIZE) -> float:
    """model, loaderを用いて学習を行い、lossを返す"""

    # dataloaderを生成
    loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)

    # 学習モードに設定
    model.train()

    train_running_loss = 0.0

    for data in loader:
        # デバイスの指定
        ids = data["ids"].to(device)
        mask = data["mask"].to(device)
        labels = data["labels"].to(device)

        # labelsの次元を(BATCH_SIZE, 1)から(BATCH_SIZE,)に変更
        labels = labels.squeeze(1)

        optimizer.zero_grad()

        outputs = model(ids, mask)
        # dataloader_xでの損失の計算/
        loss = criterion(outputs, labels)
        # 勾配の計算
        loss.backward()
        optimizer.step()

        # 訓練データでの損失の平均を計算する
        train_running_loss += loss.item() / len(loader)

    return train_running_loss


def calculate_loss_and_accuracy(model, dataset, device=None, criterion=None):
    """損失・正解率を計算"""
    # 評価モードに設定
    model.eval()

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False)

    loss = 0.0
    total = 0
    correct = 0

    with torch.no_grad():
        for data in dataloader:
            # 変数の生成、device送り
            ids = data["ids"].to(device)
            mask = data["mask"].to(device)
            labels = data["labels"].to(device)

            # 順伝播
            outputs = model(ids, mask)

            # labelsの次元を(BATCH_SIZE, 1)から(BATCH_SIZE,)に変更
            labels = labels.squeeze(1)

            # 損失計算
            loss += criterion(outputs, labels).item()

            # 正解率計算
            pred = torch.argmax(outputs, dim=-1)
            total += len(labels)
            correct += (pred == labels).sum().item()

    return loss / len(dataset), correct / total


def make_graph(value_dict: dict, value_name: str, bn: int, method: str) -> None:
    """value_dictに関するgraphを生成し、保存する。"""
    for phase in ["train", "test"]:
        plt.plot(value_dict[phase], label=phase)
    plt.xlabel("epoch")
    plt.ylabel(value_name)
    plt.title(f"{value_name} per epoch at bn{bn}")
    plt.legend()
    plt.savefig(f"{method}_{value_name}_bn{bn}.png")
    plt.close()


if __name__ == "__main__":

    DEBUG = False
    if DEBUG:
        print("DEBUG mode")

    METHOD = "bert"

    # 時間の計測開始
    start_time = time.time()

    # seedの固定
    seed_everything(use_torch=True)

    # Colab
    # PATH = '/content/drive/MyDrive/NLP100'
    # local
    PATH = "../../"

    # deviceの指定
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print(f"Use {device}")

    # データの読み込み
    train = pd.read_csv(f"{PATH}/ch06/50/train.txt", sep="\t", index_col=0)
    test = pd.read_csv(f"{PATH}/ch06/50/test.txt", sep="\t", index_col=0)

    # indexを再設定
    train = train.reset_index(drop=True)
    test = test.reset_index(drop=True)

    # 計算の短縮
    if DEBUG:
        train = train.sample(1000).reset_index(drop=True)
        test = test.sample(1000).reset_index(drop=True)

    # 正解データの生成
    cat_id_dict = {"b": 0, "t": 1, "e": 2, "m": 3}
    y_train = train["CATEGORY"].map(cat_id_dict)
    y_test = test["CATEGORY"].map(cat_id_dict)

    # tokenizerの読み込み
    tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
    max_len = 10

    # datasetの作成
    dataset_train = TextDataset(train["TITLE"], y_train, tokenizer, max_len, device)
    dataset_test = TextDataset(test["TITLE"], y_test, tokenizer, max_len, device)
    # {'input_ids': [101, 2885, 6561, 24514, 2391, 2006, 8169, 2586, 102, 0], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]}
    # BERTでは、変換の過程で元の文の文頭と文末に特殊区切り文字である[CLS]と[SEP]がそれぞれ挿入されるため、それらも101と102として系列に含まれています。0はパディングを表します。

    # パラメータの設定
    DROP_RATE = 0.4
    OUTPUT_SIZE = 4
    BATCH_SIZE = 32
    NUM_EPOCHS = 30
    if DEBUG:
        NUM_EPOCHS = 2
    LEARNING_RATE = 2e-5

    # モデルの定義
    model = BERTClass(DROP_RATE, OUTPUT_SIZE).to(device)

    # criterion, optimizerの設定
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE)

    train_losses = []
    train_accs = []
    test_losses = []
    test_accs = []

    for epoch in tqdm(range(NUM_EPOCHS)):
        # 学習
        train_running_loss = train_fn(
            model, dataset_train, device, optimizer, criterion, BATCH_SIZE
        )
        print(train_running_loss)

        # 損失と正解率の算出
        train_loss, train_acc = calculate_loss_and_accuracy(
            model, dataset_train, device, criterion
        )
        test_loss, test_acc = calculate_loss_and_accuracy(
            model, dataset_test, device, criterion
        )

        train_losses.append(train_loss)
        train_accs.append(train_acc)

        test_losses.append(test_loss)
        test_accs.append(test_acc)

        # 20epoch毎にチェックポイントを生成
        if epoch % 20 == 0:
            torch.save(model.state_dict(), f"89_{METHOD}_epoch{epoch}.pth")
            torch.save(
                optimizer.state_dict(),
                f"89_{METHOD}_optimizer_epoch{epoch}.pth",
            )

    # グラフへのプロット
    losses = {"train": train_losses, "test": test_losses}

    accs = {"train": train_accs, "test": test_accs}

    make_graph(losses, "losses", bn=BATCH_SIZE, method=METHOD)
    make_graph(accs, "accs", bn=BATCH_SIZE, method=METHOD)

    print(f"train_acc: {train_acc: .4f}")
    print(f"test_acc: {test_acc: .4f}")

    # 計測終了
    elapsed_time = time.time() - start_time
    print(f"elapsed_time:{elapsed_time:.0f}[sec]")

結果は下記のようになりました。RNNよりも全般的に悪い点が気になります。 また、この中ではALBERTが一番良いのも気になりますね。

model train_acc test_acc time(s)
BERT 0.6765 0.6304 4221
RoBERTa 0.4191 0.4505 4047
ALBERT 0.7848 0.7391 6941(K80)
CANINE 0.4191 0.4505 4944

感想

RNNのinput,outputやBERTの正確なoutputなどの理解がない状態で挑んだため、この章は時間がかかりました。 ただ、やり切ったおかげで簡単なモデルであればすぐに作れるようになったかなと思います。やってよかった自然言処理100本ノック。

「象は鼻が長い」を依存構造解析してみた

はじめに

「象は鼻が長い」という二重主語問題の例文について知り、自然言語処理における解析器はどのように解析するのか知りたくなったため、簡単に調べてみることにしました。

おかしい点があれば指摘頂けると助かります。

何が問題か

「象は鼻が長い」という日本語を聞いて、意味が分からない方はいないと思います。

では、「象は鼻が長い」の主語はなんでしょうか? 「象」でしょうか。そうすると「鼻」は何になるでしょうか?私は分かりません。

では「鼻」を主語と考えると、今度は「象」は何にあたるでしょうか。・・・分からないですね。

この例文は、日本語文法を説明する上で説明が難解な文章としてよく例に挙げられます。 同様の文章には、「私はうなぎだ」「こんにゃくは太らない」などがあります。

これらの理解・解説は「象は鼻が長い」(三上章 (著))を読むか、ゆる言語学ラジオさんの説明が分かりやすかったため参照にして下さい。

www.9640.jp

www.youtube.com

依存構造解析を行ってみる

依存構造解析にはspaCy/GiNZAを利用します。

参照:https://acro-engineer.hatenablog.com/entry/2019/12/06/120000

  • spacy==2.3.2
  • ginza==4.0.6

象は鼻が長い

コードは下記のような感じです。

import spacy

nlp = spacy.load('ja_ginza')
doc = nlp('象は鼻が長い')

for sent in doc.sents:
    for token in sent:
        print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)
    print('EOS')

出力結果はこちら

0 象 象 NOUN 名詞-普通名詞-一般 dislocated 4
1 は は ADP 助詞-係助詞 case 0
2 鼻 鼻 NOUN 名詞-普通名詞-一般 nsubj 4
3 が が ADP 助詞-格助詞 case 2
4 長い 長い ADJ 形容詞-一般 ROOT 4
EOS

表示している項目は、左から単語・見出し語・品詞タグ・品詞情報・依存関係ラベル・係り先の単語インデックスです。

面白そうな情報は全て出していますが、依存構造解析で必要な情報は、「dislocated」「case」「nsubj」などの依存関係ラベルと係り先の単語インデックスの欄の情報です。 これらはそれぞれ下記のような意味となります。

  • nsubj: 主格で述語に係る名詞句。
  • case: 助詞による格の表示。
  • dislocated: 文の通常の中核的な文法関係を満たしていない前置または後置の要素に用いられる。(原文の英語を翻訳したもの)

この依存関係ラベルはUniversal Dependencyにて定義されているラベルです。

詳細はこちら。

https://www.anlp.jp/proceedings/annual_meeting/2015/pdf_dir/E3-4.pdf

dislocatedに関してはこちら。

https://universaldependencies.org/u/dep/dislocated.html

よって、下記のようなことが分かります。

  • 「象」の係り先は「長い」だが、「dislocated」なため、文法関係を満たしていない
  • 「鼻」の係り先は「長い」だが、「nsubj」のため、「鼻」が主語であり、「長い」が述語と解析されている。

この場合は「鼻」が主語になるんですね。「象」は「長い」がdislocatedと判定されるのは凄いと感じました。

私はうなぎだ

同様に、「私はうなぎだ」について

0 私 私 PRON 代名詞 nsubj 2
1 は は ADP 助詞-係助詞 case 0
2 うなぎ うなぎ NOUN 名詞-普通名詞-一般 ROOT 2
3 だ だ AUX 助動詞 cop 2

「私」が主格であり、nsubjのため「うなぎ」(うなぎだ)を述語と解析しています。 この場合は、文字通り「I'm an eel.」の意味になるんでしょうか。

こんにゃくは太らない

次に、「こんにゃくは太らない」について

0 こんにゃく こんにゃく NOUN 名詞-普通名詞-一般 nsubj 2
1 は は ADP 助詞-係助詞 case 0
2 太ら 太る VERB 動詞-一般 ROOT 2
3 ない ない AUX 助動詞 aux 2

「こんにゃく」が主格であり、nsubjのため「太ら」を述語と解析しています。 こんにゃく自体が太らない、という解析になっているように感じます。

分かったこと

現在の依存構造解析器では、言葉の通りの解析を行うことが多いことが分かりました。 当然の結果ですが、構造解析をより理解できて個人的に面白いトピックでした。

将来的には三上さんの文法での解析器なども登場するのかもしれません。ワクワクですね。

自然言語処理〔改訂版〕 (放送大学教材)を読みました

自然言語処理〔改訂版〕 (放送大学教材)を読んだので、その感想をまとめます。

www.kinokuniya.co.jp

なぜ読んだか

自然言語処理を仕事で使うことが増えたのですが、知識がほぼなかったため基礎から学びたいと思っていました。

チームの自然言語に詳しい方から、何冊かおすすめの本を紹介してもらいその中で最も良さそうだと感じたため、読むことを決めました。

感想

自然言語処理という分野を広く簡単に学ぶのに最適な本だと感じました。 自然言語処理の発展の流れや、ターニングポイントとなる技術が簡潔な表現でまとまっていました。 更に昨今の機械学習の発展まで網羅しています。attention周りまで簡単に載っていますが、TransformerやBERTなどは載っていません。

また、学び始める前は、自然言語処理のタスクには文章分類、固有表現抽出、検索、機械翻訳ぐらいしかすぐには思いつきませんでしたが、 本書を読むことで、意味解析、構文解析、文脈解析など幅広いタスクが存在し、その基本となるところを学ぶことができました。

一方、各タスクの詳細や、アルゴリズムの深い理解については他の情報源を参照しながら学ぶ必要があると思います。 加えて、プログラムでの実装に関しても別途学ぶ必要があります。

言語処理学会第28回年次大会の参加、もし可能であれば投稿・発表も狙っているので、 学会にて本書で学んだ知識が活かせるといいなと思っています。

www.anlp.jp