kaggleのメルカリ価格予測コンペの反省とword2vec、Embeddingについて

そういえば年末年始あたりにメルカリのコンペに冬休みの自由研究として参加してました

他のことに追われていたらいつの間にかコンペが終了したので反省という名の手法の振り返りをする

コンペ自体の詳細は以下のリンクから

Mercari Price Suggestion Challenge | Kaggle

何をするコンペだったかというと主催側で商品名、商品の品質(5段階)、商品のカテゴリ名や説明文などが100万件以上あるデータを提供するのでそこから与えられた商品の価格を予測してねっていう感じのコンペ

価格の予測というわけで二値分類とかではないから半教師分類が使えなくて困った(値段予測を10ドル区切りのnクラス分類と置けばゴリ押しできたかも)

まずは自分が行ったデータ分析をば。コードはここ

与えられた訓練用のデータtrain.tsvの欠損値の確認を最初に行うtsvファイルで与えられたので読み込みで最初こけた

# 欠損値の確認
train_df.isnull().sum()

---
train_id                  0
name                      0
item_condition_id         0
category_name          6327
brand_name           632682
price                     0
shipping                  0
item_description          4
dtype: int64

訓練データは140万件ほどある中、brand_nameは63万件も欠損値があることがわかる。
学歴の偏差値でもそうだが、シャネルやルイヴィトンといったブランド名が付与されているだけである程度の価格は検討つきそうという直感があったので、brand_nameが付与されているか否かの二値の値を新たに与えることにした

# brand_nameが存在するなら1を返し、無いなら0を返す
def isBrandname(x,name):
    brand_name = str(x['brand_name'])
    if(brand_name==name):
        return 0
    return 1

fill_nan_brand_name = "Unknown"
train_df['brand_name']=train_df[['brand_name']].fillna(fill_nan_brand_name)
train_df['is_brand_name'] =  train_df.apply(lambda x: isBrandname(x,fill_nan_brand_name), axis=1)

次にcategory_nameの中身を確認した。value_countで中身を見てみると一部でこんな感じ
f:id:Owatank:20180322111441p:plain

カテゴリ数が1って140万もデータあるのになんか嫌だな・・・one-hot絶対したくねえ・・・と思い悩む。
カテゴリの書式はどれも/でsplitできそうだと考えて左から1つsplitしたものを取ってそれをfirst_category_nameという新たなデータとして加えることにする

def get_first_category_name(x):
    category_name = str(x['category_name'])
    return category_name.split('/')[0]

# category_nameの第一カテゴリを抜き取る
# 欠損値は Unknown で埋めておく
fill_nan_category_name = "Unknown"
train_df['category_name']=train_df[['category_name']].fillna(fill_nan_category_name)

train_df['first_category_name'] =  train_df.apply(lambda x: get_first_category_name(x), axis=1)

first_category_namevalue_countは次のようになった

f:id:Owatank:20180322112005p:plain

カテゴリを一つ抜き取ったので全体として意味があるものというのは失われてしまった
カテゴリは3つくらいsplitできそうだったので同様にsecond_category_namethird_category_nameも加えた

ここでグのサマーインターンのときに盗み聞きした懇親会での言葉を思い出す。
日付という文字列のデータから日付を得るように、nameから何かしら得ることはできないだろうか

ということでbrand_nameが無いものに対してnameからブランド名を抜き取ってそれをbrand_nameとして与えてあげることにする

brand_dict = dict(train_df['brand_name'].value_counts())

def extract_brandname_from_name(x,b_dict):
    name = str(x['name'])
    name_split_list = name.split(' ')
    if(x['is_brand_name'] != 1):
        for item in name_split_list:
            if(item in b_dict):
                return item
    return str(x['brand_name'])

train_df['new_brand_name'] =  train_df.apply(lambda x: extract_brandname_from_name(x,brand_dict), axis=1)

new_brand_nameという名前で新たに作ったので、これに対しても最初に行ったブランド名があるか無いかの処理を同様に行った
結果としては10万件ほどブランド名があるものが増えた。

だいたいこんな感じでデータの分析を終えた。自分ではこれがまだ精一杯(´・ω・)

分析して新たに作ったデータで予測を行う学習モデルを構築する

# 入力層
input_dim = 8
X = tf.placeholder(tf.float32, shape=[None, input_dim], name="input")
t = tf.placeholder(tf.float32, shape=[None, 1])
# パラメータ1
stddev = np.sqrt(2.0 / input_dim)
input_node_num = 128
W1 = tf.Variable(tf.truncated_normal([input_dim,input_node_num], stddev=stddev))
b1 = tf.Variable(tf.constant(0.1, shape=[input_node_num]))

# パラメータ2
stddev = np.sqrt(2.0 / input_node_num)
hidden1_node_num = 256
W2 = tf.Variable(tf.truncated_normal([input_node_num,hidden1_node_num], stddev=stddev))
b2 = tf.Variable(tf.constant(0.1, shape=[hidden1_node_num]))

# パラメータ3
stddev = np.sqrt(2.0 / hidden1_node_num)
hidden2_node_num = 512
W3 = tf.Variable(tf.truncated_normal([hidden1_node_num,hidden2_node_num], stddev=stddev))
b3 = tf.Variable(tf.constant(0.1, shape=[hidden2_node_num]))

# パラメータ3
stddev = np.sqrt(2.0 / hidden2_node_num)
output_W = tf.Variable(tf.truncated_normal([hidden2_node_num,1], stddev=stddev))
output_b = tf.Variable(tf.constant(0.1, shape=[1]))
keep_prob1 = tf.placeholder(tf.float32) # ドロップアウトする割合
keep_prob2 = tf.placeholder(tf.float32) # ドロップアウトする割合

layer1 = tf.nn.relu(tf.matmul(X,W1) + b1)

layer2 = tf.nn.relu(tf.matmul(layer1,W2) + b2)
layer2_drop = tf.nn.dropout(layer2, keep_prob1)
layer3 = tf.nn.relu(tf.matmul(layer2_drop,W3) + b3)
layer3_drop = tf.nn.dropout(layer3, keep_prob2)

output_layer = tf.matmul(layer3_drop,output_W)+output_b

p = tf.nn.relu(output_layer,name="output")
# 荷重減衰
norm_term = tf.nn.l2_loss(layer1) + tf.nn.l2_loss(layer2) +tf.nn.l2_loss(layer3)
# 正則項
lambda_ = 0.0001
# 損失関数(MSE)
loss = tf.reduce_mean(tf.square(p - t))+ lambda_*norm_term
#loss = tf.reduce_mean(tf.square(p - t))
# 学習アルゴリズム
optimizer = tf.train.AdamOptimizer()
train_step = optimizer.minimize(loss)

いつも通りのMLPで構築してみた。

しかし結果のスコアは0.713ほどだった。(値が小さい方がいいからクッソ悪い)
何故だと悩んで、そういえばitem_description使ってないな・・・と思った。
他の参加者はitem_descriptionを使用しているのだろうか?と思い、カーネルを漁ることに

するとこんな感じのコードを見つけた

tok_raw = Tokenizer()
tok_raw.fit_on_texts(raw_text)

train_df["seq_item_description"] = tok_raw.texts_to_sequences(train_df.item_description.str.lower())
test_df["seq_item_description"] = tok_raw.texts_to_sequences(test_df.item_description.str.lower())
train_df["seq_name"] = tok_raw.texts_to_sequences(train_df.name.str.lower())
test_df["seq_name"] = tok_raw.texts_to_sequences(test_df.name.str.lower())

!?
え、何これは・・・

層の定義においてはこんなだった

emb_name = Embedding(MAX_TEXT, 50)(name)
emb_item_desc = Embedding(MAX_TEXT, 50)(item_desc)

!!?!?

Embeddingって何だ・・・と当時の自分は思ったが、試しにこれらを使ってnameitem_descriptionを特徴として使いつつ、自分で得た新たなデータも加えて学習させてみるかと試して見たところ、スコアが0.4656とハチャメチャ上がった

当時は全くもって謎だったが、最近自然言語処理について調べたりしていたのでここで定義していたEmbeddingはまずEmbed自体の意味が埋め込みという意味で、埋め込みというのは文字を実数のベクトルに変換して特徴として扱えるようにするって感じなのかなと自分で考えた。

だから最初にtok_raw.texts_to_sequencesitem_descriptionの文章を単語に分割したり、ストップワードの排除やステミングといった前処理をかけてあげたりしていたのだ・・・

埋め込みによるベクトルの変換においてはappleなら[1,0,0,0,0,...]といったone-hotのような変換ではなく、apple = 果物+赤+...といった単語の足し算で表現するようにして[1,0,0,1,0,...]としたり、あえて単語に重みをつけて表現することもできるそうだ

最近読んだポアンカレ埋め込みという論文では、word2vecではユークリッド空間による埋め込みを行っているが、双曲線空間の中に埋め込むことで、ユークリッド空間の埋め込みより単語同士の類似性、つまり距離や潜在的な階層表現などにおいてより優れる結果を出せると書いてあった。しかもベクトルによる表現も倹約に表現できるらしい。

双曲線空間すげえ!!!!!!!!!!!

無敵じゃんこんなの・・・概要だけでめちゃくちゃワクワクしたし銀河を感じてしまった・・・
しかしこの論文で出てきた最適化方法あたりでリーマン最適化、測地線、などなど聞いたこともない単語がたくさん出てきて全然わからなかった
どうやら噂の多様体というものを知る必要があるらしい。

今まで機械学習って確率統計とか最適化数学あたりが重要なんかなーと思っていたが、自然言語処理において幾何学ってこんなロマンがあるし、こういった問題に対して登場してくるのかと思える良い発見ができるコンペだった。結果はボロクソだったが