読者です 読者をやめる 読者になる 読者になる

にほんごのれんしゅう

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

前処理にディープラーニングを使う

前処理にディープラーニングを使う

目的

  • スクレイパーなどで集めた画像には、ターゲットとする画像以外必要ないケースが度々ある
  • データセットづくりと呼ばれる画像からノイズ画像を取り除くスクリーニングの作業の簡略化の必要性
  • 画像のスクリーニングを機械学習でやってしまおうという試みです

前処理そのものにディープラーニングを投入する

  • 画像処理において、学習したい画像かどうかをスクリーニングすることは膨大なコストがかかるので、この作業自体を自動化したい
  • 今回はスクレイパーでいい加減にあつめたグラビア女優の画像7万枚超えを、手動でスクリーニングするのは極めて困難なので、VGG16を転移学習させてフィルタを作っていきます
  • 一枚10円で500枚のペア(positiveとnegative)のデータセットを知り合いのニートに作ってもらう
  • ニートの作成したデータセットをもとに、転移学習させてフィルタを構築

システム構成図

図1. システム構成図

人間との比較

  • 実は人間よりどれくらい早くできるかとうことも検証したくて、自分の目で見て判断して分類していくのと、機械ではどの程度の差があるか試した
  • 人間は6時間で5000枚ぐらいのチェックが限界であった(精神的に大いに疲弊する)
  • 対して75000枚をGTX 1080 2基で 50分位である。圧倒的に機械学習の方がよい

ネットワークの出力の特性を知っておく

  • 活性化関数や最小化する目的関数の設計は実にバラエティに富んでおり、組み合わせは考え始めると無数にあるように見える
  • 内部がリニアであり、そのロジットを取ったロジスティック回帰が確率表現として優秀なのでよく使う
  • softmax, categorical crossentropyとかは出力値を寄せきってしまうので、あまり確率表現に向いていないように見える
  • 今回はロジットを使う

過学習の防止

  • どの程度、データセットにフィッティングさせていくかかがかなり重要なので、訓練データとバリデーションデータに分けて未知のデータセットに対しても汎化性能を確認する
  • 今回はepochごとにmodelを保存してベストなモデルを探索することで選んでいった -> 最適は85epochぐらいがよかった

しきい値の決定

  • 0~1に変化する値である
  • 当然0.5がしきい値であるが0.5から始めていき、しきい値を調整する

図2. しきい値 0.5を上回った画像

図3. しきい値 0.5を下回った画像

-> いろいろ調整たが、多めにスクリーニングするとして、しきい値を0.65とした。

感想

  • 作ろう作ろうと思って後回しにしてた案件です
  • できてよかったです
  • ニートは社会的評価も本人評価も色々めんどくさく、ご機嫌をとるのが大変だったので、やるならクラウドワークスのほうが良さげです

全体の流れ

コードはgithubにおいておきます。非商用・研究目的では好きに使ってください
bitbucket.org bitbucketよくわかってないので、何か不具合があればtwitterで教えていただけると幸いです。

$ git clone https://${YOUR_ID?}@bitbucket.org/nardtree/maeshori-toolkit-for-deeplearning.git

step1. 入力サイズに合わせて変形する

ニートから帰ってきたデータは500のpositive,negativeのフォルダに別れたデータセットであった
フォルダ名を答えとして、224×224のサイズに変形する。この時単純な変形にしてしまうと縦横比が崩壊してしまうので維持する細工を入れる。
実行

$ python3 image-resizer.py --gravia_noisy

コード

def gravia_noisy():
  target_size = (224,224)
  dir_path = "./gravia-noisy-dataset/gravia/*/*"
  max_size = len(glob.glob(dir_path))
  for i, name in enumerate(glob.glob(dir_path)):
    if i%10 == 0:
      print(i, max_size, name)
    save_name = name.split("/")[-1]
    type_name = name.split("/")[-2]
    if Path("gravia-noisy-dataset/{type_name}/{save_name}.minify" \
        .format(type_name=type_name, save_name=save_name)).is_file():
      continue
    try:
      img = Image.open(name)
    except OSError as e:
      continue
    w, h = img.size
    if w > h :
      blank = Image.new('RGB', (w, w))
    if w <= h :
      blank = Image.new('RGB', (h, h))
    try:
      blank.paste(img, (0, 0) )
    except OSError as e:
      continue
    blank = blank.resize( target_size )
    os.system("mkdir -p gravia-noisy-dataset/{type_name}".format(type_name=type_name))
    blank.save("gravia-noisy-dataset/{type_name}/{save_name}.mini.jpeg" \
      .format(type_name=type_name, save_name=save_name), "jpeg" )

step2. 学習する

最終的にはResNetを使うが、速度がほしい前処理のタスクのためVGG16で学習を行う
softmaxでなくて、sigmoid + binary_crossentropyです
実行

$ python3 deep_gravia_maeshori.py --train

コード

from keras.applications.vgg16 import VGG16
def build_model():
  input_tensor = Input(shape=(224, 224, 3))
  model = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

  dense  = Flatten()( \
             Dense(2048, activation='relu')( \
               BN()( \
                model.layers[-1].output ) ) )
  result = Activation('sigmoid')( \
              Dense(1, activation="linear")(\
                 dense) )

  model = Model(input=model.input, output=result)
  for layer in model.layers[:11]:
    if 'BatchNormalization' in str(layer):
      ...
    else:
      layer.trainable = False
  model.compile(loss='binary_crossentropy', optimizer='adam')
  return model

step3. 全体のデータセットに適応する

適切にフォルダに画像を配置して行ってください
実行

$ python3 deep_gravia_maeshori.py --classify

コード

def classify():
  os.system("mkdir ok")
  os.system("mkdir ng")
  model = build_model()
  model = load_model(sorted(glob.glob('models/*.model'))[-1])
  files = glob.glob("bwh_resize/*")
  random.shuffle(files)
  for gi, name in enumerate(files):
    try:
      img = Image.open('{name}'.format(name=name))
    except FileNotFoundError as e:
      continue
    img = [np.array(img.convert('RGB'))]
    if not os.path.exists(name):
      continue
    result = model.predict(np.array(img) )
    result = result.tolist()[0]
    result = { i:w for i,w in enumerate(result)}
    for i,w in sorted(result.items(), key=lambda x:x[1]*-1):
      if w > 0.65:
        os.system("mv {name} ok/".format(name=name))
      else:
        os.system("mv {name} ng/".format(name=name))
      print(gi, name, w, file=sys.stderr)

評価極性辞書の構築

評価極性辞書の構築

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

  極性の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. 未知の言語を翻訳する

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



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