peiprog’s blog

情報科学を学ぶ大学4年生。主にプログラミング、情報系の話題を書いていきます。

pythonで機械学習(kerasのOneHotレイヤーの作り方)

こんにちは、peiprogです。
今回はkerasで学習時にOneHotベクトル化するレイヤーの作り方を書きます。

テキスト分類などでは、学習の前に特徴ベクトル化するとメモリを大量に消費してしまい、メモリ不足で動かなくなることがあります。それなら学習前はOneHotベクトルのフラグの立つインデックスのみを保持し、学習時にOneHotベクトル化をしたほうがメモリ効率は断然いいです。


実装方法

Lambdaレイヤは引数に関数オブジェクトを渡し、レイヤーオブジェクトのような振る舞いをさせるレイヤです。
Lambdaレイヤにkerasのbackedのone_hot関数を渡すことでデータをOneHot化するレイヤを追加できます。
Lambdaの引数のarguments['numclasses']は1つのOneHotベクトルの次元数で、input_shapeは1つのデータにOneHotベクトルをいくつ保持するかを表します。そのため、output_shapeは(data_length, vec_size)とします。

  from keras.models import Sequential
  from keras.layers.recurrent import LSTM
  from keras import backend as K
  from keras.layers.core import Lambda, Dense, Activation
  def init_model(self, vec_size, data_length, output_num):
    self.model = Sequential()
    # OneHot化するレイヤー
    self.model.add(Lambda(K.one_hot, dtype='int32', arguments={'num_classes': vec_size},
                                    input_shape=(data_length, ), output_shape=(data_length, vec_size))

    self.model.add(LSTM(output_num, activation='relu', dropout=0.5))
    self.add(Dense(output_num))
    self.model.add(Activation('softmax'))


そのため、1つの入力データは長さがdata_lengthで、フラグの立つインデックスを保持した配列になります。

IT系就活についての話

 こんにちは、peiprogです。私と友達の話や経験を元にIT系(主に独立系SIer、Web企業)の就活に向けてやっておくといいことをまとめてみました。

目次

この記事の対象とする人
軽く自己紹介
なにをしておくといい?
まとめ


この記事の対象とする人

 この記事は
  • 独立系SIer、もしくはweb企業を目指す人
  • プログラミングや情報技術について少しは勉強してきたが、すごい知識や実績、成果物とかがあるわけではない
 という人を対象にしてます。また、Web系企業は少ししか受けていないので偏った情報かもしれません



軽く自己紹介

 情報科学専攻の大学4年生です。内定先は超大手Web企業(一度も使ったこと無い人は多分いないようなところ)と大手独立系SIerです。就活は3年の10月から少しずつはじめて4年の4月で終了しました。



なにをしておくといい?

やっておくといいことは
  • チームでの開発経験
  • IPAの資格
  • 集大成となるような成果物を作る
です。
 チームでの開発経験はハッカソンプログラマーのアルバイト、インターンや友達とアプリ作るなどです。独立系のSIerでは大抵チームでの開発経験の有無を聞かれました。作ったプログラムの内容、自分の担当した役割、具体的になにをしたかをまとめておきましょう。

 IPAの資格は基本情報、応用情報技術者などのことです。基本情報はあって当然、応用情報はしっかり勉強してきているという反応がありました。知識をどれくらい持っているかは面接などを通して全て伝えるのは難しいので、一定の知識量はあるという証明になる資格は持っていると良いです。

 集大成となるような成果物は、参考書のサンプルを変えただけのようなものではなく、利用する側を考慮したものを作るといいです。企業によっては成果物を提出させ選考対象にしたり、プレゼンさせることもあります。これは特にWeb企業に多かったです。そうでなくてもどういうものを開発してきたかということは技術者面接では大抵聞かれるため、話のネタになります。



まとめ

 IT系の就活ではプログラミング未経験の文系なども受けてくるため、これらの経験は大きなアドバンテージになります。また、これらを活かすためにも自己分析やSPI対策も怠らないようにしましょう。個人的には今までしてきたことや準備が非常に大事で、就活は選考が始まる前に勝負がついていると感じました。先々後悔しないために選考が始まる前に意味のある経験を積み、準備をしっかりしましょう!

Pythonでテキストの機械学習(相互情報量を使った特徴ベクトル選定)

 お久しぶりです。peiprogです。
 今回は、ドキュメント群から生成したベクトルから、機械学習で重要な特徴ベクトルを抽出する内容です。テキストをベクトル化したものは何万次元にもなりますが、中には10000個あるドキュメントの中で1回しか出てこない単語など学習に必要のないデータが大量にあります。また、次元数が多いと学習時間が長くなり、メモリの消費量も増えます。
 そのため、今回は相互情報量に基づいて、学習に本当に必要な特徴ベクトルの抽出の仕方を考えます。

目次





相互情報量とは


 相互情報量とは、ざっくりいうと2つの事象(の集合)がどれだけ互いに関連性があるかを表す数値です。定義は以下のとおりです。
こちらにより詳しいことが載ってます。相互情報量とは

\displaystyle
I(X;Y) = \sum_{y\in{Y}}\sum_{x\in{X}}p(x,y)\log_2\frac{p(x,y)}{p(x)p(y)}




実装するアルゴリズム

 ドキュメントから生成したベクトルとラベルにおいての相互情報量は以下のようになります。Classesはドキュメントに紐付いたラベルの集合で、Wordsはベクトル化されている単語の出現頻度を表す集合です。

\displaystyle
I(Words;Classes) = \sum_{word\in{Words}}\sum_{class\in{Classes}}p(word, class)\log_2\frac{p(word,class)}{p(word)p(class)}



 今回は単語ごとのすべてのラベルの相互情報量の中で最大の数値を比較して、機械学習に使うデータを選択します。
   相互情報量を用いた以下の式を単語ごとの関連度の指標として、すべての単語でこの数値を計算します。相互情報量の特性上、値が大きいとラベルと単語の出現頻度の関連性が高いことがわかるため、値が大きければ大きいほど学習に重要な単語であると判断できます。


\displaystyle
Output(word) = MAX\Bigl(p(word, Classes)\log_2\frac{p(word,Classes)}{p(word)p(Classes)}\Bigl)




実装

 先ほど説明したアルゴリズムを実装していきます。式を展開してまとめると以下のようになります。c(〜)は該当する事象の出現数を意味します。
 単純に値を比較するだけのため、単語に依存しない数値は定数とみなして省略してあります。

\displaystyle
Output(word) = MAX\Bigl(c(word, Classes)\log_2\frac{c(doc)c(word,Classes)}{c(word)c(Classes)}\Bigl)

必要なパラメータは以下のとおりです。

c(word, class) ラベルがclassの全ドキュメント中のwordの出現数
c(class) ラベルがclassのドキュメントの数
c(word) 全ドキュメント上のwordの出現数
c(doc) ドキュメントの総数


 以下が単語ごとの相互情報量を保管したリストを返す関数です。extract_wordsメソッドは前回の記事に乗っています。
   vectorsはドキュメントをベクトル化したもののリスト、doc_listはドキュメントのリスト、label_listはドキュメントに対応するラベルのリスト、label_numはラベルの種類の数です。

def get_mi_list(vectors, doc_list, label_list, label_num):
    mi = []  # 単語ごとの相互情報量を保管するリスト
    word_sum = np.sum(vectors, axis=0)  # 全ドキュメント上の単語の出現数

    # すべてのドキュメントを単語リストに変換してラベルごとに保存
    labels_doc_words = [[] for i in range(label_num)]
    for doc_index in range(len(doc_ls)):
        labels_doc_words[labels[doc_index]].extend(extract_words(doc_ls[doc_index]))  # ドキュメントを単語のリストに変換
    # 各ラベルに属するデータ数を保管
    label_count = []
    for label in range(label_num):
        label_count.append(labels.count(label))

    # 全ワードの相互情報量を計算
    for word_index in range(len(words)):
        max_num = -100.0
        # 全ラベルの相互情報量の最大値をリストに追加
        for label in range(label_num):
            counter = labels_doc_words[label].count(words[word_index])  # ラベルがlabelの全文章中で出現するwordの数
            mi_num = counter * \
                         (np.log2(len(label_list) * counter / (label_count[label] * word_sum[word_index])))
            if not math.isnan(mi_num):
                max_num = max(mi_num, max_num)
            mi.append(max_num)

    return np.array(mi)

 実装は難しいことは無いですが、注意点はlogの中身が0以下だとNaNが返るため if not math.isnan(~)をする必要があるという点です。
 返ってきた相互情報量のリストに基づいて学習対象のデータを選定することで学習に重要な特徴ベクトルを抽出できます。


導入結果

 私が研究で作っている2値分類のニューラルネットワークのモデルを用いて今回紹介した手法を実験してみました。以下が実験の結果です。識別率、その他すべての数値が改善されているのがわかります。 
 ただしベクトル化の実行時間が大幅に伸びてしまいました。しかし、ニューラルネットワークの学習時間の比重のほうが大きいため、学習全体の時間としては大した差にはなりません。

※・導入前はTF-IDF重みでベクトル化し、値の大きい単語を選定。ベクトルの要素の値はTF-IDF
・導入後は出現数でベクトル化し、相互情報量に基づき単語を選定。ベクトルの要素の値は単語の出現数

指標(%) 導入前 導入後
識別率 92.9 95.2
精度 96.4 96.4
再現度 87.6 92.9
偽陽性 1.5 1.4
F値 91.8 94.6



Pythonでsklearn+janomeを使って特徴ベクトル抽出

 お久しぶりです。peiprogです。
 機械学習において入力データの形式はとても大事ですよね。分類手法と同じかそれ以上に大事かもしれないですね。今回はその重要な入力データの作り方(加工の仕方)についてです。テキストから機械学習に適した形の入力データに加工します。ライブラリを使うことで短く簡潔に書けます。

目次



仕組み


仕組みはこんな感じです↓
  1. 単語ごとにわける(形態素解析
  2. TF-IDFを用いてベクトル化
用語の説明は省略します。



使用するライブラリ

sklearn 機械学習ライブラリ
janome 日本語の形態素解析ライブラリ
インストール方法は以下を参照してください。pip installなどで必要なライブラリは用意してください。
pipのインストール
sklearnのインストール
janomeのインストール



下準備

 sklearnにはベクトライザーという文章からベクトルを生成できるクラスがあります。それを使うためには単語の分割方法を定義する必要があります。
 そのためまず、ベクトルを生成するために単語ごとに分ける処理(トークン化)のメソッドを定義します。ここで先ほどインストールしたライブラリのjanomeを使用します。
 以下がドキュメントを単語ごとに分割するプログラムです。与えられたドキュメントを単語に分割し、数値文字列と記号を取り除いた単語のリストを返すというメソッドです。

from janome.tokenizer import Tokenizer

def extract_words(text):
    words = []
    t = Tokenizer()  # トークン化してくれるクラス
    tokens = t.tokenize(text)  # トークン化
    for token in tokens:
        if not token.surface.isdigit() and token.part_of_speech.split(',')[0] != "記号":  # 記号と数字を取り除く
            words.append(token.surface)
    return words

 Tokenizerはトークン化してくれるクラスで、このクラスのtokenizeメソッドを使うことで単語の分割をしてくれます。
 次に取得したトークンに着目して必要な単語だけをリストに保存していきます。isdigit()は数値の文字列かを判定するメソッドです。
 また、token.part_of_speechはその単語の品詞や活用形をカンマ区切りで保持する文字列です。これをカンマで分割して0番目の文字列を比較します。0番目の文字列は品詞を表す文字列です。

Vectorizer生成+ベクトル化

次にTF-IDF重みでベクトル化をします。まず、ドキュメントをベクトル化してくれるオブジェクトvectorizerを生成します。先ほど定義したextract_wordsを引数にします。これはvectorizer側の、ドキュメントから単語に分割する際に呼び出されるメソッドを指定することを表します。
最後にvectorizer.fit_transformでテキストからベクトルに変換します。toarrayをするとnumpyの配列になります。ベクトルが見やすくなるし、データを扱いやすくなります。

from sklearn.feature_extraction.text import TfidfVectorizer

text = "お好きなドキュメントを代入してください。"
vectorizer = TfidfVectorizer(analyzer=extract_words)
vec = vectorizer.fit_transform([text]).toarray()
#vec = vectorizer.transform([text])

 また、fit_transformはベクトル化する一定のルールを設定してベクトル化を行い、transformはすでに設定されているルールに従ってベクトル化を行います。場面によって使い分けが必要ですが、詳しくはこちらを参照してください。



まとめ

 いかがでしたか?sklearnを使うと非常に簡潔に書けますね。ただ、ドキュメント数が多くなるとベクトルの次元数が大きくなり学習時間やメモリ消費量が大きくなるため、より対象のデータを絞り込む必要があります。それについても今後書きたいと考えています。最後にまとめたコードを載せておきます。

from janome.tokenizer import Tokenizer
from sklearn.feature_extraction.text import TfidfVectorizer


def extract_words(text):
    words = []
    t = Tokenizer()
    tokens = t.tokenize(text)
    for token in tokens:
        if not token.surface.isdigit() and token.part_of_speech.split(',')[0] != "記号":
            words.append(token.surface)
    return words

text = "お好きなドキュメントを代入してください。"
vectorizer = TfidfVectorizer(analyzer=extract_words)
vec = vectorizer.fit_transform([text]).toarray()






補足

 見ていただいた方でストップワードを取り除く処理をしていないじゃん?と思う人がいるかもしれません。TfidfVectorizerの中身を見たところ、analyzerに関数を代入、もしくはVectorizerのbuild_analyzerをオーバーライドするとストップワードを設定しても取り除いてくれないみたいです。
 analyzerを定義してなおかつ、ストップワードを取り除きたいのであれば、analyzerに代入する関数の中でそういう処理を記述する必要があります。ただ、analyzerとして呼び出される関数は引数が1つしかないのでストップワードを引数として渡すことができないです(^^;。ただTF-IDFであれば性質上ストップワードの重みは小さくなるので大丈夫かな~と思います。
 ちなみに英語を扱うのであれば、janomeを使った関数の代わりにanalyzerにステミング処理関数を渡す必要があります。英語のステミングはNLTKというライブラリがあるのでそれを活用するといいと思います。

Pythonでワードクラウド作ってみた

お久しぶりです、peiprogです。

今回はPythonでワードクラウドを作ってみました。ワードクラウドとは
追記:英語のみ対応です。ライブラリ(pytagcloud)が日本語に対応していないので...。日本語に対応させたいなら描画のところは自前で書く必要がありますね(^^;


全体の流れとしては

1.ドキュメント読み込み
2.ベクトル生成
3.ベクトルからワードクラウド描画
という感じです。こんな感じの画像を生成できます。
f:id:peiprog:20170614221243p:plain:w250


使うライブラリは以下の通りです。

sklearn 機械学習ライブラリ
pytagcloud ワードクラウド描画ライブラリ
numpy 数値計算ライブラリ
urllib インターネットからページを取得する用
 
 ライブラリのインストール方法についての説明は省略します。pip installなどで必要なライブラリは用意してください。
pipのインストールについて
sklearnのインストールについて
pytagcloudのインストールについて(英語サイト)
urllibのインストールについて



ドキュメント読み込み

これは単純にファイルパスを指定して読み込むだけです。

doc = open("file_path", "r").read()




ベクトル生成

ドキュメントを単語ごとに分けて、出現頻度のベクトルを生成します。


はじめにストップワードを取り除く必要があります(ストップワードとは、私、です、ます、などの出現頻度が高く不要な語)。
日本語のストップワードはインターネットなどから入手する必要があります。今回はSlothLibのページからストップワードを取得します。
SlothLibのページ

from urllib import request

page = request.urlopen('http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt')  # インターネットからページを取得
stopwords = [line.decode('utf-8').strip() for line in page]  # デコード
stopwords = [w for w in stopwords if not w==u'']  # 空文字を取り除く



 次に取得したストップワードを用いてsklearnのCountVectorizerを使ってベクトル化します。先ほど生成したストップワードとmin_dfを指定します。min_dfを指定すると、指定した値より出現回数の低い単語を無視します。
 最終的には、ワードクラウドを生成するのに必要な、単語と出現頻度ベクトルの数値を合わせたタプルを生成します。
 最後に昇順ソートをしておきます。キー関数を使用しています。キー関数についてはこちらを参考にしてください。

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(min_df=1, stop_words=stopwords)  # ベクトル生成器を生成
vector = vectorizer.fit_transform(doc)  # docからベクトルを生成
words = vectorizer.get_feature_names()  # ベクトルに対応する単語のリストを取得

words_vector = []  # 単語と出現頻度ベクトルの数値のタプルが要素となるリスト
for i in range(len(words)):
    if not words[i].isdigit():  # 数字の文字列でなければリストに追加
        words_vector.append((words[i], vector[0, i]))  # 単語とベクトルの数値をタプルとしてリストに追加
words_vector.sort(key=lambda i:i[1], reverse=True)  # 昇順ソート、キーはベクトルの値





ワードクラウド描画

生成した単語と出現頻度ベクトルを合わせたタプルのリストを用いてワードクラウドを作成します。この例では上位30個の単語をピックアップします。
また、2行目の引数のminsize, maxsizeはワードクラウド中の単語の大きさの最大値、最小値を表します。

from pytagcloud import create_tag_image, make_tags
tag = make_tags(words_vector[:30], minsize=20, maxsize=40)  # 上位30この単語を用いてワードクラウドを生成
create_tag_image(tag, savefile_path, size=(300, 300))  # 実際に描画したものを保存




まとめ

いかがでしたか?実際に画像作ってみると結構面白いですよね!PDFやワードファイルに対応できると色んなドキュメントのワードクラウドが見れて面白いかもしれませんね。
githubにコード上げてるのでよかったら見てください!
https://github.com/ippei-miyake/WordCloudGenerator


最後にまとめたコードを示します。

from urllib import request
from sklearn.feature_extraction.text import CountVectorizer
from pytagcloud import create_tag_image, make_tags


doc = open("好きなファイルパスを指定", "r").read()

page = request.urlopen(
       'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt')  # インターネットからページを取得
stopwords = [line.decode('utf-8').strip() for line in page]  # デコード
stopwords = [w for w in stopwords if not w == u'']  # 空文字を取り除く


vectorizer = CountVectorizer(min_df=1, stop_words=stopwords)  # ベクトル生成器を生成
vector = vectorizer.fit_transform([doc])  # docからベクトルを生成
words = vectorizer.get_feature_names() # ベクトルに対応する単語のリストを取得

words_vector = []  # 単語と出現頻度ベクトルの数値のタプルが要素となるリスト
for i in range(len(words)):
    if not words[i].isdigit():  # 数字の文字列でなければ
        words_vector.append((words[i], vector[0, i]))  # 単語とベクトルの数値をタプルとしてリストに追加
words_vector.sort(key=lambda i: i[1], reverse=True)  # 昇順ソート、キーはベクトルの値


tag = make_tags(words_vector[:30], minsize=20, maxsize=40)  # ワードクラウドを生成 上位30語を描画
create_tag_image(tag, "好きなファイルパスを指定", size=(300,300))  # 実際に描画したものを保存