前回、NNで翻訳を作ってみたのですが、モデルの評価等をすっ飛ばしてきました。そこで、今回はkaggleで現在開いているNLPの問題を、検証含め解いてみたいと思います。

問題内容

解いた問題はReal or Not? NLP with Disaster Tweets(https://www.kaggle.com/c/nlp-getting-started ) です。ツイート内容からを本物の災害かどうかを2値推定するというものです。精度を上げれば、どんな災害がいつどこで起きているかをツイッターの呟きから特定する、ソーシャルセンサーとして活用できる気もします。
先に結果を書いておくと、スコアは0.74539でした。

実装

モデルの概要図は下の通りです。

1
2
3
4
5
6
7
8
9
10
11
12
13
  text

embedding

Positional Embedding

Encoder Layers

linear ← embedding ← keyword

Sigmoid

出力

前回同様,attentionベースで作成しました。ただ、翻訳ではないので、使用した注意機構はself-attentionのみとなります。

全体概要

コードは ( https://github.com/gojiteji/kaggleNL ) にあげてるので、必要な部分を順を追って解説していきます。

データ作成部分

まずは辞書クラスを作ります。decoder機能(id->word変換)は使用しませんが、デバッグなど確認用に、前回同様の実装を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class Vocabulary():
def __init__(self,disabletokenize=False):
self.w2i={}
self.i2w={}
self.oov_char='<unk>'
self.pad='<pad>'
self.bos='<bos>'
self.eos='<eos>'

self.special_chars = [self.pad,self.oov_char,self.bos,self.eos]
self.data = ""
self._words=set()
self.tokenize=disabletokenize
self.tknzr = TweetTokenizer()
def update(self,text):
if(self.tokenize):
self._words.update(text)
else:
self.data=self.tknzr.tokenize(text)
#self.data=word_tokenize(text)
self._words.update(self.data)
self.w2i = {w: (i + len(self.special_chars)) for i, w in enumerate(self._words)}

self.i2w = {i: w for w, i in self.w2i.items()}
self.w2i['<pad>'] = 0
self.i2w[0] = '<pad>'
self.w2i['<unk>'] = 1
self.i2w[1] = '<unk>'
self.w2i['<bos>'] = 2
self.i2w[2] = '<bos>'
self.w2i['<eos>'] = 3
self.i2w[3] = '<eos>'


def encode(self,words):
output=[]
if(self.tokenize):
pass
else:
#words=word_tokenize(words)
words=self.tknzr.tokenize(words)
for word in words:
#辞書になし
if word not in self.w2i:
index = self.w2i[self.oov_char]#既存の<unk>を返す
else:
#辞書にあり
index = self.w2i[word]#idを引っ張ってくる
output.append(index)
return output

def decode(self,indexes):#使いどころないけど確認用
out=[]
for index in indexes:
out.append(self.i2w[index])
return out

trainデータを形態素に分解して単語を記録します。分解方法はNatural Language Toolkitのtweet tokenizerを使用しました。ハッシュタグやリプライ、顔文字も認識して分解してくれます。(最初は一般的な形態素解析ツールで分解していたため、これらが過度に分解されてしまい、文字が増えてしまいました)

次に、train,testデータをそれぞれ登録済みの単語でid化していきます。未登録の単語はid=1を振ります。今回は、学習データとしてtextとkeyを用いました。

1
2
3
4
5
keyword_voc.update(list(keywords))

for t in raw_X_train:
text_voc.update(t[0])
maxlen=158

モデル設計部分

エンコードした文章をattentionし、その出力にkeyを結合してFFを通す事で出力を得ます。attention内部の、構造については、前回の翻訳と同様です。ただ、最初にも書いた通り、翻訳対象がないため、self-attentionのみを行っています。

1
2
3
4
5
6
7
8
9
10
11
12
class AttentionNN(nn.Module):
"""" 略 """"
def forward(self,source, key):
key=self.embedding_key(key)
mask_source = self.sequence_mask(source)
hs = self.Attantions(source, mask=mask_source)
a_out=hs.reshape(len(source),self.d_model*maxlen)#attention層出力
b_out =key
out = torch.cat([a_out,b_out],dim=1)#keywordを結合
out = self.linear_out(out)
out = self.activation_out(out)
return out

学習部分

下画像のように、学習データが少しtarget=0の方が多いため、ダウンサンプリングします。
trainデータの分布

1
X_train, y_train = rus.fit_sample(raw_X_train.reshape(-1, 2) ,raw_y_train.reshape(-1, 1) )

また、test時に1/4程度、新規の単語が増えるため、エポックごとに25% ランダな位置に<unk> 記号を含ませるようにしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unks=0#unkの量
max_unks=(x_train.size - sum(sum(x_train==0)))*0.25#0以外の量
max_row=x_train.shape[0]
max_colum=x_train.shape[1]
unks=[]
while(len(unks)<max_unks):
row=int(np.random.rand()*max_row)
colum=int(np.random.rand()*max_colum)
if(not(x_train[row,colum] == 0)):#paddingじゃない
if(not(x_train[row,colum] == 2)):#bosじゃない
if(not(x_train[row,colum] == 3)):#eosじゃない
if(not( [row,colum] in unks)):#既に置き換えていなければ
x_train[row,colum]=1
unks.append([row,colum])

その他、学習部分に特別なものはありません。

結果

trainファイルの一部を検証に用いると、以下のような結果になりました。
損失関数の推移
エポックに対する正答率の推移

今回、各パラメータを

  • エンコーダレイヤ数=20
  • attentionヘッド数=6
  • text埋め込み次元=180
  • key埋め込み次元=224
  • FFの次元=190
  • 学習率=0.00001
    と設定し、20エポックほど回しました。
    提出結果、スコアは0.74539でした。まだまだ上がいるので、精進が必要そうです…