にほんごのれんしゅう

日本語として伝えるための訓練を兼ねたテクログ

評価極性辞書の構築

評価極性辞書の構築

極性辞書は別に感情を取り扱うだけのものじゃない(と思う)

  極性のPolarityという意味で軸を対象にネガティブとポジティブが存在するものをさすそうです

 よく感情をガロア理論のように何かしらの対象構造を取れるとする主張も多いのですが、わたしはこの主張に対して少し懐疑的で、果たして楽しいの反対は悲しいなのか、無数の軸がある中でどうしてそれを対称だと思ったのかなどなど色々疑問点はあります。

 すでに東北大学の乾研究室さまが、感情に関する極性に関してはプロフェッショナルであり、たまに彼らの研究成果を後追いしているレベルです。

 さて、多くの研究では最初に極性の辞書を主観評価で決定します。これは、主に何を持って悪感情か、嬉しい感情なのか不明なため人間が定義してやる必要があるのですが、ここに主観が混じるので、評価者の人間の判断に委ねられるという側面があります。

 機械学習らしく、データ量で押し切ってしまうことで、もっと簡単に文章の極性が取れるものがあるので今回ご紹介します。
 そして、感情ではなく(商品やサービスの)評価を扱います 。

Amazon, 楽天などの星は一次元情報であり、極性を構築するのに最適

 商品やサービスを気に入った場合には星が多く付き、気に入らなかったら少なくなるという単純な関係が成立しています。

 星が多い≒気に入った≒喜び、感謝
 星が少ない≒気に入らなかった≒落胆、嫌い、嫌

という仮定が入っていることにご注意してください。

ということもあって、星情報は一次元で仮定を起きやすくやりやすいデータセットであります。

星の分布について

 以前はAmazonで簡単な評価を行ったことがありますが、今回は楽天のデータセットについて行いました。
 楽天のデータセットは商品をクローリングしたものを20GByte超えのHTMLファイル(ただし、レビューは200MByte程度)を利用しました。
 単純に星5個と、星1個を比較するのが理想なのですが、残念ながらひどい非対称データとなってしまいます。そのため、星5個 vs 星3,2,1個とします。

データの公開

 最近クローリングに関しての倫理やコンプライアンスなどを読んでいると、クロールしたでデータの公開は問題ないように思えます[1,2]。ここからダウンロードできるようにしておきます。
 クローラは先日公開したKotlinの実装のものを利用しました。リミットを外して利用すると、非常に高負荷になるので、秒間1アクセス程度がやはり限度のようです。
 なお、このようにして集めたデータに関しては商用利用は原則ダメなようです。

Polarity(極性)

 ここには一部のみ記します。GISTには全て記すので、必要に応じて参照してください。
 ここから全体を参照できます。
気に入らないに入る TOP10

ダイエー -4.211883314885654
がっかり -3.724952240087231
最悪 -3.629671589687795
二度と -3.615183142062377
期待はずれ -3.364096361814979
在庫管理 -3.251811276002615
シーブリーズ -3.243134607447971
返金 -3.223751242139063
江尾 -3.142244830633572
お蔵入り -3.044963500843487
...

気にいるに入る TOP 10

本当にありがとうございました 2.330683071541743
幸せ 2.40608193615266
閉店セール 2.415456005367995
強いて 2.425465450266797
増し 2.622845298273817
5つ 2.628383278795989
モヤモヤ 2.637474892812968
ドキドキ 2.759164930673644
しいて 3.162614441418143
迫る 3.249573225146807

極性の計算の仕方

 極性の計算は割と簡単にできて、全ての単語のlog(出現頻度+1)*weightの合計値 0を下回ると否定的、0を上回ると肯定的です。
式にするとこのようなものになります。

 さらに、確率表現とするとこのようなものになります。  

データセットのダウンロードと、機械学習を一発で行うスクリプト

 Ubuntu 16.04でコンパイルした各種バイナリや、学習用データセットをダウンロード可能です    学習用データセットをダウンロードして、libsvmフォーマットに変更して、学習まで一気に行います

$ sh generate-polarity.sh

step by step. 機械学習

コードはこちらのgithubから参照できます。 github.com

step. 0 レビューデータと星数を特定のフォーマットで出力

 ダウンロードしたレビューのデータに対して、レビューデータとその時の星の数を抜き出して、あらかじめ決めたフォーマットで出力していきます。このフォーマットの形式をいかにちゃんと設計できるかも、技量だと思うのですが、ぼくはへぼいです。

{星の数} セバレータ { レビューコンテンツ }
{星の数} セバレータ { レビューコンテンツ }
….

kotlinで書くとこんな感じです。

fun rakuten_reviews(args: Array<String>) { 
  Files.newDirectoryStream(Paths.get("./out"), "*").map { name ->
    if( name.toString().contains("review") ) { 
      val doc = Jsoup.parse(File(name.toString()), "UTF-8")
      doc.select(".revRvwUserMain").map { x ->
        val star    = x.select(".revUserRvwerNum").text()
        val comment = x.select(".revRvwUserEntryCmt").text().replace("\n", " ")
        println("${star} __SEP__ ${comment}")
      }
    }
  }
}

step. 1 単語のindex付け

 機械学習で扱えるように、単語にIDを振っていきます。深層学習のチュートリアルでよくあるものですが、私がよく使う方法を記します。
 なお、今回は、Javaを使えるけどPythonは無理という方も多く、言語としての再利用性もJVM系の方が高いということで、Kotlinによる実装です。

fun termIndexer() { 
  val br = BufferedReader(FileReader("./dataset/rakuten_reviews_wakati.txt"))
  val term_index = mutableMapOf<String, Int>()
  while(true) { 
    val line = br.readLine()
    if( line == null ) break
    val ents = line.split(" __ SEP __ ")
    if( ents.size < 2 ) continue
    val terms = ents[1]
    terms.split(" ").map { x -> 
      if( term_index.get(x) == null ){ 
        term_index[x] = term_index.size + 1
        println("${x} ${term_index[x]}")
      }
    }
  }
}

step. 2 libsvmフォーマット化

 高次元データの場合、スパースで大規模なものになりやすく、この場合、Pythonなどのラッパー経由だと正しく処理できないことがあります。そのため、libsvm形式と呼ばれる形式に変換して扱います。
 直接、バイナリに投入した方が早いので、以下の形式に変換します。

1 1:0.12 2:0.4 3:0.1 ….
0 2:0.59 4:0.1 5:0.01 ...

 Kotlinで書くとこんな感じ

fun svmIndexer() {
  val term_index  = File("./dataset/term_index.txt").readText()
  val term_id     = term_index.split("\n").filter { x ->
    x != ""
  }.map { x -> 
    val (term, id) = x.split(" ")
    Pair(term, id)
  }.toMap()
  val br = BufferedReader(FileReader("./dataset/rakuten_reviews_wakati.txt"))
  while(true) { 
    val line = br.readLine()
    if( line == null ) break
    val ents = line.split(" __ SEP __ ")
    if( ents.size < 2 ) continue
    var stars = 0.0
    try { 
      stars = ents[0].replace(" ", "").toDouble()
    } catch ( e : java.lang.NumberFormatException ) { 
      continue 
    }
    val terms = ents[1]
    val term_freq = mutableMapOf<String, Double>()
    terms.split(" ").map { x -> 
      if ( term_freq[x] == null ) term_freq[x] = 0.0
      term_freq[x]  =  term_freq[x]!! + 1.0
    }
    val id_weight = term_freq.keys.map { k -> 
      Pair(term_id[k]!!, Math.log(term_freq[k]!! + 1.0) )
    }.sortedBy { x ->
      val (id, value) = x
      id.toInt()
    }.map { x ->
      val (id, value) = x
      "${id.toInt()}:${value}"
    }.joinToString(" ")
    val ans = if( stars.toDouble() > 4.0 ) 1 else if( stars.toDouble() <= 3.0 ) 0 else -1
    if( ans >= 0 ) {
      println("${ans} ${id_weight}")
    }
  }
}

step. 3 機械学習アルゴリズムにかけて、学習する

 学習アルゴリズムは割となんでもいいと思っているのですが、この前にQiitaで公開したデータに対して、素性の重要度の見方を書いてなかったので、重要度の確認の方法も兼ねて、liblinearで学習してみます。

$ ./bin/train -s 0 ./dataset/svm.fmt

 さて、これでsvm.fmt.modelというファイルができます。このファイルの中のデータが、素性の重要度と対応しておりこのようなフォーマットになっています。

solver_type L2R_LR
nr_class 2
label 1 0
nr_feature 133357
bias -1
w
-0.1026207840831818 
0.01714376979176739
....

-0.10\~\~という表記が、1ばんめの素性の重要度で、マイナスについていることがわかります。

step. 4 学習結果と、単語idを衝突させる

 単純に重みのみ書いてあるとよくわからないので、idと重みを対応づけて、わかりやすく変形します。

fun weightChecker() {
  val term_index = File("./dataset/term_index.txt").readText().split("\n").filter { x ->
    x != ""
  }.map { x -> 
    val (term, index) = x.split(" ")
    Pair(index, term) 
  }.toMap()
  File("./dataset/svm.fmt.model").readText().split("\n").filter { x -> 
   x != "" 
  }.mapIndexed { i,x -> 
    Pair(i - 6, x)
  }.filter { xs -> 
    val (i, x ) = xs
    i >= 0
  }.map { xs ->
    val (i, x ) = xs
    //println( "${term_index[i.toString()]}, ${x}" )
    Pair(term_index[(i+1).toString()], x.toDouble() )
  }.sortedBy{ xs ->
    val (term, weight) = xs
    weight
  }.map { xs ->
    val (term, weight) = xs
    println("${term} ${weight}")
  }
}

参考文献

[1] http://qiita.com/nezuq/items/c5e827e1827e7cb29011
[2] http://q.hatena.ne.jp/1282975282

Kotlinによるスクレイピング

🔱Kotlinによるスクレイピング🔱

図1. 艦これの画像をKotlinでスクレイピングした画像で作った阿武隈のモザイクアート

PythonからKotlinへ部分的な移行@機械学習エンジニアの視点

 Pythonは便利な言語です。しかし、スクリプト言語で型を厳密に評価しないということと、いくつかの高負荷な操作において、うまく行かないことがあります。
 個人的な経験によるものですが、分析対象が巨大になり、より並列性が求められるプログラムにおいては、Pythonの再現性のないエラーについて悩まされることが多かったです。
 何気なく触ってみたKotlinは結構使いやすく、Python3で実装していたScraperを移植してみました。
 (なお、私はJavaをろくに触ったことがないです)

Pythonのthreadとmultiprocessをつかったスクレイパー

図2. ずっとPythonで使ってたScraperのアクセス方式

PythonではマルチプロセスとThreadは区別されており、Threadはマルチコアにならないです(1CPUしか使えないのでこの細工がいる)。

Kotlinニュービーなのですが、PythonのthreadとKotlinのthreadは動作が違うように見えます。
KotlinのThreadではプロセスが分割しないのに、CPUの使用率が100%を超えるため、複数CPUを使った効率的なThreadingが行われているようです。(つまり、Multiprocessで分割したあと、Threadを配下で実行する必要がなさそう)

Kotlinのインストール

Ubuntuでのインストールの方法です。

$ curl -s https://get.sdkman.io | bash
$ sdk install kotlin

メモリを増やす

環境変数JAVA_OPTにJVMのメモリが記されているらしく、普通に使うと、メモリ不足で落ちるので、今風になおしておいたほうがいいでしょう。 わたしはこのような設定にしています。

JAVA_OPTS="-Xmx3000M -Xms3000M"

CUIでのコンパイル&実行のしかた

 わたしはJavaが苦手で、Javaを極力避けるようなキャリアプランを歩もうと思っていたのですが、それは主にエクリプスやIDEやらのツール自体の学習コストが多いように感じていて、つらいと思うことが多かったからです。
 KotlinもIDEを使えば便利なのですが、CUIコンパイル&実行で問題が生じない限りはCUIでいいやと思っています。
 コンパイルには、いろいろあるのですが、runtimeを含めて、jarファイルに固めてしまうのがユーザビリティ高めでした。

$ kotlinc foo.kt -include-runtime -d foo.jar

 これでコンパイルができます。
 複数のファイルをまとめてjarにすることができます。(foo.ktでbar.ktの関数やClassを参照できます)

$ kotlinc foo.kt bar.kt -include-runtime -d foo.jar

 JavaMavenなどでコンパイルして得ることができるjarファイルをclasspathに追加することで利用可能です。 (alice.jar, bob.jarというファイルを利用するとします)
 多くのJavaの資産を再利用することができるので、たいへん助かります。

$ kotlinc foo.kt bar.kt -cp alice.jar:bob.jar -include-runtime foo.jar

 例えば、外部のjarファイルを利用したkotlinのjarを実行する際にはこのようなコマンドになります。

$ kotlin -cp alice.jar:bob.jar:foo.jar FooKt

 このFooKtという名称はmain関数が含まれるfoo.ktファイルを指定するのに用いるようです。    

JavaScriptをつかったサイトはphantomjs, selenium, jsoupの組みあせが楽

 JavaScriptによる非同期のデータ読み込みがある場合、単純に取得してjsoupなどで解析するだけだと、コンテンツが取れません
 JavaScriptを動作させて人間が見ていると同じような状態を作らないといけないので、selenium経由でphamtomjsを動作させてJavaScriptを動作させます。
 たとえば、MicrosoftBingの画像検索は、Ajaxで描画されており、JavaScritpが動作しない環境では動作できません。(実験的な目的ですので、実際に画像をスクレイピングする際は、API経由で行ってください)

    val driver = PhantomJSDriver()
    driver.manage().window().setSize(Dimension(4096,2160))
    driver.get("https://www.bing.com/images/search?q=${encoded}")
    //すべての画像が描画されるのを待つ
    Thread.sleep(3001)
    val html = driver.getPageSource() 

 htmlという変数に、JavaScriptが動作したあと描画された状態のhtmlが入ります。これをjsoupに入れることで、各種画像のsrcのURLがわかります。
 発見した画像のURLを元に、wgetコマンドで任意のディレクトリ以下のフォルダに格納します。

    val doc  = Jsoup.parse(html.toString(), "UTF-8")
    println(doc.title())
    doc.select("img").filter { x ->
       x.attr("class") == "mimg"
    }.map { x ->
       val data_bm = x.attr("data-bm")
       val src = x.attr("src")
       Runtime.getRuntime().exec("wget ${src} -O imgs/${name}/${data_bm}.png")             
    }

 PhantomJSはこのサイトからコンパイル済みのバイナリをダウンロードして、PATHが通った場所においておく必要があります。

Thread

 いくつか書き方があるようですが、もっとも簡単にできた実装です。
スクレイピングするロジック全体を{}で囲った部分がthreadのインスタンスになって、そのthreadをstartさせたり,joinしたりして並列に動かすことができます。  

    val threads = url_details.keys.map { url ->
      val th = Thread {
        if(url_details[url]!! == "まだ") { 
          _parser(url).map { next ->
            urls.add(next)
          } 
          println("終わりに更新 : $url")
          url_details[url] = "終わり"
          // save urls
          _save_conf( mapper.writeValueAsString(url_details) )
        }
      } 
      th
    }

オブジェクトのシリアライズ、デシリアライズ

 jacksonというシリアライズモジュールが限定的に使えるようです。
 Javaのライブラリだけではうまくいかないようで、Kotlin用のモジュールを別途読み込む必要があります。
 限定的というのは、MutableMap<String, DataClass>をシリアライズ、デシリアライズしてみたところうまく行きませんでした。
MutableMap<String, String>はうまく行くので、ネスト構造がだめか、Data Classに対応してないかよくわかっていないです。
 シリアライズの例

val mapper = ObjectMapper().registerKotlinModule()
val serialzied = mapper.writeValueAsString(url_details) 

 デシリアライズの例

val mapper = ObjectMapper().registerKotlinModule()
val url_details = mapper.readValue<MutableMap<String, String>>(json)

実際にスクレイピングしてみる

まずgit cloneする

$ git clone https://github.com/GINK03/kotlin-phantomjs-selenium-jsoup-parser.git

幅優先探索

 今のところ、二種類のスクレイピングまで実装が完了していて、単純にJavaScriptを評価せずに、幅優先探索で、深さ100までスクレイピングをする。
(自分のサイトのスクレイピングに使用していたため、特に制限は設けていませんが、50並列以上の並列アクセスで標準でアクセスするので、適宜調整してください。)

$ sh run.scraper.sh widthSearch ${yourOwnSite}

画像探索

 Microsoft Bingを利用して、画像検索画面で検索します。
 (実験的にAPIを使わないでAjaxで描画されるコンテンツを取れるかどうか、チャレンジしたコードですので、大量にアクセスして迷惑をかけてはいけないものだと思います)
検索リストは、github上のkancolle.txtのファイルを参考にしてください。

sh run.scraper.sh image ${検索クエリリスト} ${出力ディレクトリ}

ソースコード

github.com

word2vec, fasttextの差と実践的な使い方

word2vec, fasttextの差と実践的な使い方

目次

  • Fasttextとword2vecの差を調査する
  • 実際にあそんでみよう
  • Fasttext, word2vecで行っているディープラーニングでの応用例
  • 具体的な応用例として、単語のバズ検知を設計して、正しく動くことを確認したので、紹介する
  • Appendix

(発表用の資料も掲載いたします,小さくて見づらいので、直リンはこちら)

原理の表面的な説明

  • Skip gramではある特定の単語の前後の単語の出現確率を測定することでベクトル化する



図1. ある目的の単語から、周辺の単語の確率を計算してベクトル化する

  • Word2vecとfasttextではこれを実装したもの
  • ただし、fasttextにはsubwordという仕組みが入っている



図2. softmaxで共起確率を計算する

あそんでみよう

2017年2~3月のTwitterのデータ3GByteを学習させたデータがあるので、遊んでみよう
学習、予想用のプログラムをgithubに、学習済みのmodelをpython3のpickle形式でdropboxにおいてある



ファイルが置いてあるDropboxのリンク
github.com

プロジェクトはこちら

Word2vecで遊ぶ方法

ただの言葉の相関の他、言語の足し算引き算した結果、何の単語に近くなるか計算できます。



図3. word2vecの単語ベクトルの足し算引き算の例

fasttextで遊ぶ方法

ただの言葉の相関の他、言語の足し算引き算した結果、何の単語に近くなるか計算できます。



fasttextとword2vecとの違い

  • fasttextはsubword分割という単語の中の一部が近いと近くなる特性がある



図4. 文脈(skip gram)に依存せず近くなるので、好ましく働くことも、好ましくなく働くこともある



  • Word2Vecで“艦これ”の関連度を計算すると、同じような文脈で用いられる、他のゲームタイトルが多く混じってしまう
  • これはメリットなのか、デメリットなのか、使用用途でわかれそう



単語の演算の違い
  • Word2Vecの特徴として、単語の演算が謎理論(理論的な裏付けが無いように見える)で演算できる
  • fasttextもベクトル表現なので、足し算・引き算が可能なので比較する
  • fasttextとw2vで結果が異なる



fasttext, word2vecの実践的な使い方

  • CNN, RNNなどのディープラーニングの素性とする
  • 例えば、100万語で、10単語の文章の判別問題の際、one-hotを利用すると、壊滅的なテンソルサイズになりGPUに乗らない
  • そこで意味関係を内包しているという仮説がある、fasttext, w2vを使うことで、256次元程度にシュリンクできる



RNN(or CNN)の出力をfasttext, word2vecの出力に近似して使う場合

  • Deep Learningの出力、特にRNNのテキスト生成系のモデルにおいて、出力次元が爆発してしまう問題に対応するため、出力
  • 出力をLinear + mean square errorとすることで、直接ベクトルを当てに行くことができる(復元するにはconsine類似度などで逆引きする必要がある)



応用例:言葉の進化(バズ)を観測する

  • 言葉はバズると使用法が変化する
  • 今までの主流は単語の出現頻度の変化の観測
  • 単語の使われ方の変化を観測する



  • 2次元にエンベットされていると仮定すると、図のようになる



  • さらに時間系列でベクトルを表現すると下図のようになる



  • 技術的な課題点の解決
    • 問題点:エンベッティングの際、初期値依存性があり、ベクトルが回転したり、端によったりすると歪んだりする。
    • 解決策:絶対座標に変換するため、基準となる単語を選択(16000単語前後)
    • 解決策:基準となる単語郡からのコサイン類似度の変化量を各観測したい単語ごとに作成
    • 解決策:このベクトルをZとする



  • Z(プ)ベクトル(長いので略す)をデイリーで作成していき、n日のZ(プ)ベクトルをZ(プ,n)ベクトルとする
  • Z(プ,n)ベクトルとZ(プ,n-1)ベクトルとのユークリッド距離を計算する



Dの大きさが大きいほど、使用法が変化したと考えることができ、バズや言葉の進化を定量的に観測することができる。

  • これを日にちのタイムシリーズでグラフを描画すると下図のようになる


例えば、意味の変化量が少ない「ソシャゲ」という単語と、激しく文脈が変化した「プレミアムフライデー」という単語の変化量Dは大きく違う

Tweet取得・分析のシステム構成



Appendix.1. 対応関係を学ぶ

  • 図を見ると企業と企業の代表者の関係をみると、一定の法則があることがわかる



  • この性質(Distributional inclusion hypothesis)を利用して、logistic-regressionなどで、関係を学習することが可能である

Appedix.2. 未知の言語を翻訳する

  • 出現する単語の並びの関連に相関があるので、言語が異なっても似たような分布になる



文字の文化が共通していれば、翻訳可能?

emotion2vec

emotion2vec

テキスト感情ベクタライザの提案 〜 doc2vec, fasttext, skipthoughtに続く第四のテキストベクタライザ 〜
ジョーク投稿です。正確なリプレゼンテーションかどうか、十分な検証をする必要があります)

感情は難しい

 コミュ症の人が何らか相手の感情を察しようとした場合、既存の知識や経験に基づいて相手が何を意図しているのか、把握することが難しいという悲しい事実があります。
 ディープラーニングってやつでなんとかして!案件ですね。

f:id:catindog:20170322223403p:plain
ここでは、感情を定量的に解析して確認する手法を提案します。

リプレゼンテーション(表象)の選択

 最も端的に感情を示している素性は何でしょうか。映像ではその人の行動であったり、表情であったりします。
 テキストの世界では、最近の若い人たちは、絵文字というものを使うことがあります。絵文字はその文脈では伝えきれてない感情を絵文字の表現力を借りて表現する場合で有効であります。

例) 
💢 -> ムカつく
❤ -> 好き好き
♪ -> 気分が上昇している

 なるほど。漫画はよく読むのでよくわかりますね。なんで漢字の書き取りがあって、絵文字の書き取りは小学校ではやらないんですかね。
 テキストにおいては、絵文字がそのテキストの秘められたリプレテーション(表象)になっているように仮説が立てられそうです。

手法

 Word Level CNNを用いて、テキストをベクトル化します。ベクトル次元数は2014次元で大きいです。
 Twitterのフリープランで400万ツイートものツイートを頑張って集めました。これを分かち書きして、fasttextで分散表現に置き直します。
 分散表現で記された文章をCNNにかけて、そのツイートに内在されていたはずの絵文字を予想するタスクを与えます。複数の絵文字がある場合、複数予想させます。
 結果として、感情を示すベクトルが獲得できるようになります。

  (epochは前回CNNでのテキスト認識で用いたoverfitにならない25epochを適応)
 optimizer:adam
  window size:1,2,3,4,5
  input-vector:256dim * 30dim
  output-vector:2048dim
 その他諸々はコードを参照ください

f:id:catindog:20170322230510p:plain

イメージ図。Ubuntuのimpressで作ったが見切れてる

コード

(商用利用する際には一声くださると助かります)
github.com

結果

 今回のemotion2vecは極端な不均衡データでありました。そもそも感情は自然に分布しないと思うので、それはいいのですが、特定の♪などの絵文字リプレゼンテーションになりがちになりました。
(テキストは小説家になろうの「蜘蛛ですが何か」からお借りしました)

text.ないわー。 
 😭 87 😂 5 💢 4 😳 32 💓 2 🙄 2 💗 22 😑 222 😊 2 👍 1 😢 1 💕 11 😇 1 💩 1
(悲しくて起こっている感情のベクタ) 
text.これで私は自由だー!
 ♡ 765 💕 5 😭 4 😂 4 😊 4 💓 3 👍 3 💗 3 🏻 2 😆 2 😍 22 🙌 2 😳 1 🙏 111 😌 1 
(自由の感情のベクタ)
text.《熟練度が一定に達しました。スキル『蜘蛛糸LV3』が『蜘蛛糸LV4』になりました》 
★ 945843 
(なんか事務口調のテキストには星がいっぱいつきます)
text.あ、でも蜘蛛猛毒だと大抵の相手は即死しちゃうな。
😱 8 💢 8 😭 7 😨 7 😡 6 💥 6 😇 55 💦 5 😂 4 😅 4 🏻 4 🙏 43
(ムンクっぽい何か)
text.マジかー。
😭 16 😂 6 😇 3 😳 3 😍 2 😢 2 😱 2 💕 22 🙄 1 💗 1 💓 1 💢 11 🙏 1 💦 1 🙈 1 😌 1 😅 1 
(マジかー)
text.「クソッ!?」
♡ 17 💢 10 💩 921111
(うんちのお気持ち)
text. うるせぇ、てめえ、ぶっ殺すぞ 
💢 99
(わかる) 
text. あのあの、エッチなのはダメなのです
♡ 257 💕 3 💦 3 😭 322 😂 2 😢 2 😊 11 😅 1 😇 1 😑 1 😌 1 
(ちゃんとハートと汗があるよ、電ちゃんっぽい発言を意図しました)

考察

Word LevelじゃなくてChar Levelのほうがよかった気がする。
強い感情に引っ張られて大きなウェイトがつくので、感情の起伏を定量的に記すのもいいかもしれない

まとめ

 じつはこれはText GAN(SeqGAN)の副産物だったりします。SeqGANにはとにかく前処理が山ほどあって、なんとなく学習までのイメージがあるのですが、遠いです💦