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
で中身を見てみると一部でこんな感じ
カテゴリ数が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_name
のvalue_count
は次のようになった
カテゴリを一つ抜き取ったので全体として意味があるものというのは失われてしまった
カテゴリは3つくらいsplitできそうだったので同様にsecond_category_name
とthird_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
って何だ・・・と当時の自分は思ったが、試しにこれらを使ってname
とitem_description
を特徴として使いつつ、自分で得た新たなデータも加えて学習させてみるかと試して見たところ、スコアが0.4656とハチャメチャ上がった
当時は全くもって謎だったが、最近自然言語処理について調べたりしていたのでここで定義していたEmbedding
はまずEmbed
自体の意味が埋め込みという意味で、埋め込みというのは文字を実数のベクトルに変換して特徴として扱えるようにするって感じなのかなと自分で考えた。
だから最初にtok_raw.texts_to_sequences
でitem_description
の文章を単語に分割したり、ストップワードの排除やステミングといった前処理をかけてあげたりしていたのだ・・・
埋め込みによるベクトルの変換においてはapple
なら[1,0,0,0,0,...]
といったone-hotのような変換ではなく、apple = 果物+赤+...
といった単語の足し算で表現するようにして[1,0,0,1,0,...]
としたり、あえて単語に重みをつけて表現することもできるそうだ
最近読んだポアンカレ埋め込みという論文では、word2vecではユークリッド空間による埋め込みを行っているが、双曲線空間の中に埋め込むことで、ユークリッド空間の埋め込みより単語同士の類似性、つまり距離や潜在的な階層表現などにおいてより優れる結果を出せると書いてあった。しかもベクトルによる表現も倹約に表現できるらしい。
双曲線空間すげえ!!!!!!!!!!!
無敵じゃんこんなの・・・概要だけでめちゃくちゃワクワクしたし銀河を感じてしまった・・・
しかしこの論文で出てきた最適化方法あたりでリーマン最適化、測地線、などなど聞いたこともない単語がたくさん出てきて全然わからなかった
どうやら噂の多様体というものを知る必要があるらしい。
今まで機械学習って確率統計とか最適化数学あたりが重要なんかなーと思っていたが、自然言語処理において幾何学ってこんなロマンがあるし、こういった問題に対して登場してくるのかと思える良い発見ができるコンペだった。結果はボロクソだったが