Kaggle - ConnectX(1) - 4枚そろえるボードゲーム

今回からConnect Xコンペに参加したいと思います。

Connect X

Connect Xは、ボードゲームの一種で上からコインを交互に落として、縦か横か斜めに4枚コインをそろえた方が勝ちというルールになっています。

ConnectX Getting Startedというスタート練習用のノートブックがありますのでこれを試しに実行してみます。

Kaggle環境のインストール

下記バージョンのKaggle環境をインストールする必要があるようです。

Connect Xはバージョンに依存する環境なんでしょうかね。

[ソース]

1
!pip install 'kaggle-environments>=0.1.6'

[結果]

問題なくインストールすることができました。

Connect X環境の作成

Connect X環境を作成します。

[ソース]

1
2
3
4
5
from kaggle_environments import evaluate, make, utils

env = make("connectx", debug=True)
env.render()

エージェントの作成

エージェントを作成します。

ここではサンプルとしてランダムにコインを落とす場所を決めているようです。

今後はこのロジックを実装していき勝率を上げていけばいいんですね。

[ソース]

1
2
3
4
# This agent random chooses a non-empty column.
def my_agent(observation, configuration):
from random import choice
return choice([c for c in range(configuration.columns) if observation.board[c] == 0])

エージェントのテスト

上記で作成したエージェントのテストを行います。

ただ相手のロジックもランダムにコインを落とすようなので・・・・今回はただの動作確認用です。

[ソース]

1
2
3
4
env.reset()
# Play as the first agent against default "random" agent.
env.run([my_agent, "random"])
env.render(mode="ipython", width=500, height=450)

[結果]

上記のようなボードが現れてコインが次々に落とされ、コインが4つそろったら終了になります。

アニメーションとして動くのでちょっとおもしろいです。

エージェントのテストと訓練

エージェントのテストと訓練を行います。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
# Play as first position against random agent.
trainer = env.train([None, "random"])

observation = trainer.reset()

while not env.done:
my_action = my_agent(observation, env.configuration)
print("My Action", my_action)
observation, reward, done, info = trainer.step(my_action)
# env.render(mode="ipython", width=100, height=90, header=False, controls=False)
env.render()

[結果]

一回の動作(どの位置にコインを落とすのか)ごとに、その動作がデバッグ表示されます。

エージェントの評価

作成したエージェントを評価します。

[ソース]

1
2
3
4
5
6
def mean_reward(rewards):
return sum(r[0] for r in rewards) / float(len(rewards))

# Run multiple episodes to estimate its performance.
print("My Agent vs Random Agent:", mean_reward(evaluate("connectx", [my_agent, "random"], num_episodes=10)))
print("My Agent vs Negamax Agent:", mean_reward(evaluate("connectx", [my_agent, "negamax"], num_episodes=10)))

[結果]

ランダム選択の相手との結果と、NegaMax法の相手との結果(平均報酬)が表示されます。

ゲーム終了時に勝つと 1 が、負けると 0 が、どちらでもない場合 (引き分け・勝負がついていない) だと 0.5 が報酬として得られるとのことなので、ランダム相手にはたまたま勝ち越し、NegaMax相手には全敗したということになります。

エージェントと対戦

手動でエージェントとの対戦ができます。

[ソース]

1
2
# "None" represents which agent you'll manually play as (first or second player).
env.play([None, "negamax"], width=500, height=450)

[結果]

マス目をクリックしてみたのですが、「Processing…」と表示されたまま動作しませんでした。

提出ファイルの書き出し

提出用のファイルを出力します。

[ソース]

1
2
3
4
5
6
7
8
9
import inspect
import os

def write_agent_to_file(function, file):
with open(file, "a" if os.path.exists(file) else "w") as f:
f.write(inspect.getsource(function))
print(function, "written to", file)

write_agent_to_file(my_agent, "submission.py")

[結果]

問題なく出力されました。

提出ファイルのチェック

提出用のエージェント同士で対戦させて、提出ファイルの妥当性をチェックするようです。

なぜ妥当性をチェックする必要があるかというと、「完全にカプセル化されていてリモート実行できることを確認するため」???とのことでした。

[ソース]

1
2
3
4
5
6
7
8
9
10
# Note: Stdout replacement is a temporary workaround.
import sys
out = sys.stdout
submission = utils.read_file("/kaggle/working/submission.py")
agent = utils.get_last_callable(submission)
sys.stdout = out

env = make("connectx", debug=True)
env.run([agent, agent])
print("Success!" if env.state[0].status == env.state[1].status == "DONE" else "Failed...")

[結果]

エラーになってしまいました。

メソッドがないという意味かと思いますが、仕様の変更があったのでしょうか。

おいおい調査していきたいと思います。

一通りConnect Xの動作方法が分かったので、これから少しずつ調査・実装・改善を行っていきます。

Kaggle - 災害ツイートについての自然言語処理(6) - Baselineモデルでの予測

Natural Language Processing with Disaster Tweetsrに関する6回目の記事です。

Natural Language Processing with Disaster Tweets

今回はBaselineモデルを使って予測を行い、最後にKaggleに結果を提出します。

Baselineモデルの準備

Baselineモデルを作成します。

最適化アルゴリズムとしてはAdamを使います。

Adamは、移動平均で振動を抑制するモーメンタム と 学習率を調整して振動を抑制するRMSProp を組み合わせています。

また、embedding_matrixは単語ごとにベクター値を設定したものです。(前回記事をご参照ください)

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
model = Sequential()

embedding=Embedding(num_words, 100, embeddings_initializer=Constant(embedding_matrix),
input_length=MAX_LEN, trainable=False)

model.add(embedding)
model.add(SpatialDropout1D(0.2))
model.add(LSTM(64, dropout=0.2, recurrent_dropout=0.2))
model.add(Dense(1, activation='sigmoid'))

optimzer = Adam(learning_rate=1e-5)

model.compile(loss='binary_crossentropy',optimizer=optimzer,metrics=['accuracy'])

model.summary()

[結果]

データ分割

ツイートデータを配列化したものを(tweet_pad)、訓練用のデータ(正解ラベルとそれ以外)と検証用のデータ(正解ラベルとそれ以外)に分割します。

[ソース]

1
2
3
4
5
6
train=tweet_pad[:tweet.shape[0]]
test=tweet_pad[tweet.shape[0]:]

X_train,X_test,y_train,y_test = train_test_split(train,tweet['target'].values,test_size=0.15)
print('Shape of train',X_train.shape)
print("Shape of Validation ",X_test.shape)

[結果]

学習

分割したデータを使って学習を行います。(少々時間がかかります。)

[ソース]

1
history=model.fit(X_train,y_train,batch_size=4,epochs=15,validation_data=(X_test,y_test),verbose=2)

[結果]

最終的な正解率(検証用)は78.02%となりました。

提出用ファイルの作成

提出用のCSVファイルを作成します。

提出のサンプルファイル(sample_submission.cs)を一旦読み込んで、targetに予測した結果(災害ツイートかどうか)を上書いています。

[ソース]

1
2
3
4
5
6
sample_sub=pd.read_csv('../input/nlp-getting-started/sample_submission.csv')

y_pre = model.predict(test)
y_pre = np.round(y_pre).astype(int).reshape(3263)
sub = pd.DataFrame({'id':sample_sub['id'].values.tolist(), 'target':y_pre})
sub.to_csv('submission.csv',index=False)

[結果]

正解率は78.60%となりました。

それなりの結果かと思いますが、やはりいつもの8割の壁というものを感じてしまいます。

Kaggle - 災害ツイートについての自然言語処理(5) - 単語ベクター化

Natural Language Processing with Disaster Tweetsrに関する5回目の記事です。

Natural Language Processing with Disaster Tweets

今回は、単語ベクター化モデルの一つであるGloVeを試してみます。

単語分割

まずはツイートを単語に分割します。

単語分割する際にword_tokenizeメソッドを使うと、カンマや疑問符といった記号、アポストロフィによる短縮形にもうまく対応することができます。

またisalphaメソッドは文字列中のすべての文字が英字で、かつ 1 文字以上ある場合に真を返します。

[ソース]

1
2
3
4
5
6
7
8
def create_corpus(df):
corpus=[]
for tweet in tqdm(df['text']):
words=[word.lower() for word in word_tokenize(tweet) if((word.isalpha()==1) & (word not in stop))]
corpus.append(words)
return corpus

corpus = create_corpus(df)

[結果]

単語ベクター化

GloVeの学習済みモデルを準備します。3つの次元(50 D ,100 D, 200 D)が用意されていますが、今回は100 Dを使います。

処理後半のpad_sequencesメソッドでは、要素の合わない配列に対して、0 で埋めることで配列のサイズを一致させています。

paddingは前後どちらを埋めるか、truncatingは長いシーケンスの前後どちらを切り詰めるかを指定する引数で、今回は’post’(後ろ)を指定しています。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
embedding_dict={}
with open('../input/glove-global-vectors-for-word-representation/glove.6B.100d.txt','r') as f:
for line in f:
values = line.split()
word = values[0]
vectors = np.asarray(values[1:],'float32')
embedding_dict[word] =vectors
f.close()

MAX_LEN = 50
tokenizer_obj = Tokenizer()
tokenizer_obj.fit_on_texts(corpus)
sequences = tokenizer_obj.texts_to_sequences(corpus)

tweet_pad = pad_sequences(sequences, maxlen=MAX_LEN, truncating='post', padding='post')

word_index = tokenizer_obj.word_index
print('Number of unique words:',len(word_index))

[結果]

ユニークな単語数は20342となりました。

単語ベクター配列作成

単語ごとのベクター値を取得し、結果をembedding_matrixに格納します。

[ソース]

1
2
3
4
5
6
7
8
9
10
num_words = len(word_index) + 1
embedding_matrix = np.zeros((num_words, 100))

for word,i in tqdm(word_index.items()):
if i > num_words:
continue

emb_vec = embedding_dict.get(word)
if emb_vec is not None:
embedding_matrix[i] = emb_vec

[結果]

次回はBaseline Modelを使って、災害ツイートどうかの判定を行い、結果を提出してみます。

Kaggle - 災害ツイートについての自然言語処理(4) - データクレンジング

Natural Language Processing with Disaster Tweetsrに関する4回目の記事です。

Natural Language Processing with Disaster Tweets

今回はデータクレンジングを行います。

学習データと検証データの結合

まずは一括でデータクレンジングするために、学習データと検証データを結合します。

[ソース]

1
2
df = pd.concat([tweet,test])
df.shape

URLの排除

URLの排除を行います。正規表現を使ってURLパターンに合致したものを排除します。

[ソース]

1
2
3
4
5
6
7
8
example = "New competition launched :https://www.kaggle.com/c/nlp-getting-started"

def remove_URL(text):
url = re.compile(r'https?://\S+|www\.\S+')
return url.sub(r'', text)

remove_URL(example)
df['text'] = df['text'].apply(lambda x : remove_URL(x))

[結果]

HTMLタグの排除

HTMLタグも正規表現を使って排除します。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
example = """<div>
<h1>Real or Fake</h1>
<p>Kaggle </p>
<a href="https://www.kaggle.com/c/nlp-getting-started">getting started</a>
</div>"""

def remove_html(text):
html = re.compile(r'<.*?>')
return html.sub(r'',text)
print(remove_html(example))

df['text'] = df['text'].apply(lambda x : remove_html(x))

[結果]

絵文字の排除

絵文字も正規表現を使って排除します。

複数パターンある場合は、リスト型を使ってまとめて指定することができます。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Reference : https://gist.github.com/slowkow/7a7f61f495e3dbb7e3d767f97bd7304b
def remove_emoji(text):
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F" # emoticons
u"\U0001F300-\U0001F5FF" # symbols & pictographs
u"\U0001F680-\U0001F6FF" # transport & map symbols
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
u"\U00002702-\U000027B0"
u"\U000024C2-\U0001F251"
"]+", flags=re.UNICODE)
return emoji_pattern.sub(r'', text)

remove_emoji("Omg another Earthquake 😔😔")

df['text'] = df['text'].apply(lambda x: remove_emoji(x))

[結果]

句読点の排除

句読点を排除します。下記の方法で文字の変換を行っています。

  1. str.maketrans()でstr.translate()に使える変換テーブルを作成する。
  2. str.translate()で文字列内の文字を変換する。

[ソース]

1
2
3
4
5
6
7
8
def remove_punct(text):
table = str.maketrans('','', string.punctuation)
return text.translate(table)

example = "I am a #king"
print(remove_punct(example))

df['text'] = df['text'].apply(lambda x : remove_punct(x))

[結果]

スペル修正

最後にスペルの修正を行います。

pyspellcheckerというライブラリを使うので、まずこれをインストールしておきます。

[コマンド]

1
2
!pip install pyspellchecker

[結果]

 

スペルチャックは一旦単語ごとに分解し、スペルミスがあれば正しいスペルに修正し、最後に修正した単語を含めて結合し直しています。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from spellchecker import SpellChecker

spell = SpellChecker()
def correct_spellings(text):
corrected_text = []
misspelled_words = spell.unknown(text.split())
for word in text.split():
if word in misspelled_words:
corrected_text.append(spell.correction(word))
else:
corrected_text.append(word)
return " ".join(corrected_text)

text = "corect me plese"
correct_spellings(text)

[結果]


今回は、英語文字列のクレンジングを行いました。

次回は、単語ベクター化モデルの一つであるGloVeを試してみます。

Kaggle - 災害ツイートについての自然言語処理(3)

Natural Language Processing with Disaster Tweetsrに関する3回目の記事です。

Natural Language Processing with Disaster Tweets

今回は単語ごとの解析を行います。

単語解析

ツイートを単語ごとに分割する関数を定義します。

引数のtargetには災害関連(=1)か災害に関係ない(=0)かを渡します。

[ソース]

1
2
3
4
5
6
7
def create_corpus(target):
corpus=[]

for x in tweet[tweet['target']==target]['text'].str.split():
for i in x:
corpus.append(i)
return corpus

災害に関係のないツイート(target=0)を単語ごとに分割し、出現頻度が多い順にグラフ化します。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
corpus = create_corpus(0)

dic=defaultdict(int)
for word in corpus:
if word in stop:
dic[word] += 1

top = sorted(dic.items(), key=lambda x:x[1], reverse=True)[:10]

x,y=zip(*top)
plt.bar(x,y)

[結果]

the、a、toという冠詞、前置詞の出現が多いようです。


次に災害に関係のあるツイート(target=1)を単語ごとに分割し、出現頻度が多い順にグラフ化します。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
corpus=create_corpus(1)

dic=defaultdict(int)
for word in corpus:
if word in stop:
dic[word]+=1

top=sorted(dic.items(), key=lambda x:x[1],reverse=True)[:10]

x,y=zip(*top)
plt.bar(x,y)

[結果]

こちらもthe、in、ofという冠詞、前置詞の出現が多いようです。

句読点

今度は句読点について調べていきます。

string.punctuation(6行目)は、英数字以外のアスキー文字(句読点含む)を表します。

災害に関係のあるツイートの句読点出現回数をグラフで表示します。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
plt.figure(figsize=(10,5))
corpus = create_corpus(1)

dic = defaultdict(int)
import string
special = string.punctuation
for i in (corpus):
if i in special:
dic[i] += 1

x,y = zip(*dic.items())
plt.bar(x,y)

[結果]


災害に関係のないツイートの句読点出現回数をグラフで表示します。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
plt.figure(figsize=(10,5))
corpus = create_corpus(0)

dic = defaultdict(int)
import string
special = string.punctuation
for i in (corpus):
if i in special:
dic[i] += 1

x, y = zip(*dic.items())
plt.bar(x,y,color='green')

[結果]

どちらも1番目が-(ハイフン)、2番目が|(パイプ)と同じですが、3番目は:(コロン)と?(クエスチョン)と少し違いがあるようです。

共通する単語(ストップワード以外)

最後にストップワードを含まない共通する単語を調べます。

ストップワードとは、自然言語を処理するにあたって処理対象外とする単語のことです。

「at」「of」などの前置詞や、「a」「an」「the」などの冠詞、「I」「He」「She」などの代名詞がストップワードとされます。

ストップワードに含まれない単語を抽出し、グラフ化します。

[ソース]

1
2
3
4
5
6
7
8
9
10
counter = Counter(corpus)
most = counter.most_common()
x=[]
y=[]
for word,count in most[:40]:
if (word not in stop) :
x.append(word)
y.append(count)

sns.barplot(x=y, y=x)

[結果]

まだハイフンやアンパサンドなどがあるので、さらにデータクレンジングをする必要がありそうです。

N-gram解析

N-gram解析とは、対象となるテキストの中で、連続するN個の表記単位(gram)の出現頻度を求める手法です。

そうすることによって、テキスト中の任意の長さの表現の出現頻度パターンなどを知ることができるようになります。

N=2(bigram)として、N-gram解析を行います。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
def get_top_tweet_bigrams(corpus, n=None):
vec = CountVectorizer(ngram_range=(2, 2)).fit(corpus)
bag_of_words = vec.transform(corpus)
sum_words = bag_of_words.sum(axis=0)
words_freq = [(word, sum_words[0, idx]) for word, idx in vec.vocabulary_.items()]
words_freq =sorted(words_freq, key = lambda x: x[1], reverse=True)
return words_freq[:n]

plt.figure(figsize=(10,5))
top_tweet_bigrams=get_top_tweet_bigrams(tweet['text'])[:10]
x,y=map(list,zip(*top_tweet_bigrams))
sns.barplot(x=y,y=x)

[結果]

これに関しても、URLに関するものが多かったり、前置詞・冠詞の組み合わせが多かったりと、まだまだデータクレンジングが必要そうです。

というわけで、次回はデータクレンジングを行っていきます。

Kaggle - 災害ツイートについての自然言語処理(2)

Natural Language Processing with Disaster Tweetsrに関する2回目の記事です。

Natural Language Processing with Disaster Tweets

今回はツイート(text)に関するEDA(探索的データ解析)を行います。

ツイートの探索的データ解析

基本的なテキスト分析として次の3点を調べます。

  • 文字数
  • 単語数
  • 平均単語レングス

まずは文字数をカウントしグラフ化します。

災害関連かどうかでグラフを分けています。

[ソース]

1
2
3
4
5
6
7
8
9
fig,(ax1,ax2)=plt.subplots(1,2,figsize=(10,5))
tweet_len = tweet[tweet['target']==1]['text'].str.len()
ax1.hist(tweet_len,color='red')
ax1.set_title('disaster tweets')
tweet_len=tweet[tweet['target']==0]['text'].str.len()
ax2.hist(tweet_len,color='green')
ax2.set_title('Not disaster tweets')
fig.suptitle('Characters in tweets')
plt.show()

[結果]

両グラフともほとんど同じ分布になっています。

120語から140語付近がもっとも度数が多いようです。


次に、単語数をカウントしグラフ化します。

[ソース]

1
2
3
4
5
6
7
8
9
fig,(ax1,ax2)=plt.subplots(1,2,figsize=(10,5))
tweet_len=tweet[tweet['target']==1]['text'].str.split().map(lambda x: len(x))
ax1.hist(tweet_len,color='red')
ax1.set_title('disaster tweets')
tweet_len=tweet[tweet['target']==0]['text'].str.split().map(lambda x: len(x))
ax2.hist(tweet_len,color='green')
ax2.set_title('Not disaster tweets')
fig.suptitle('Words in a tweet')
plt.show()

[結果]

こちらも似た分布にはなっていますが、単語数15のところの災害関連ツイートが妙に少なくなっていることが分かります。


最後に、平均単語レングスをカウントしグラフ化します。

[ソース]

1
2
3
4
5
6
7
8
fig,(ax1,ax2)=plt.subplots(1,2,figsize=(10,5))
word=tweet[tweet['target']==1]['text'].str.split().apply(lambda x : [len(i) for i in x])
sns.distplot(word.map(lambda x: np.mean(x)),ax=ax1,color='red')
ax1.set_title('disaster')
word=tweet[tweet['target']==0]['text'].str.split().apply(lambda x : [len(i) for i in x])
sns.distplot(word.map(lambda x: np.mean(x)),ax=ax2,color='green')
ax2.set_title('Not disaster')
fig.suptitle('Average word length in each tweet')

[結果]

こちらもほぼ同じ分布になっているように見えます。

文字数、単語数、平均レングスでは災害関連かどうかを判断するのは難しいのかもしれません。

次回は語尾の単語や句読点、よく使われる単語などを調べていきます。

Kaggle - 災害ツイートについての自然言語処理(1)

今回からはNatural Language Processing with Disaster Tweetsrコンペに参加していきたいと思います。

Natural Language Processing with Disaster Tweets

簡単に説明すると、ツイートが実際の災害に関するものか否かを判定するコンペとのことです。

ちなみに自然言語処理とは、「人間が日常的に使っている自然言語をコンピュータに処理させる一連の技術であり、人工知能と言語学の一分野」ということです。

探索的データ解析 (Exploratory data analysis)

探索的データ分析を行います。つまりどんなデータかを確認していきたいと思います。

まずは必要なライブラリをインポートします。

(Kaggleノートブックで動作確認しています。)

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from nltk.corpus import stopwords
from nltk.util import ngrams
from sklearn.feature_extraction.text import CountVectorizer
from collections import defaultdict
from collections import Counter
plt.style.use('ggplot')
stop=set(stopwords.words('english'))
import re
from nltk.tokenize import word_tokenize
import gensim
import string
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from tqdm import tqdm
from keras.models import Sequential
from keras.layers import Embedding,LSTM,Dense,SpatialDropout1D
from keras.initializers import Constant
from sklearn.model_selection import train_test_split
from keras.optimizers import Adam

訓練データと検証データを読み込みます。

訓練データの一部を表示してデータを確認します。

[ソース]

1
2
3
tweet= pd.read_csv('../input/nlp-getting-started/train.csv')
test=pd.read_csv('../input/nlp-getting-started/test.csv')
tweet.head(10)

[結果]

keywordとlocationが欠損値ばかりです。

targetが予測すべき項目(1が災害関連。0が災害に無関係)なので、ほとんど残りのtext(ツイート)から予測することになりそうです。

検証データの一部も表示してみます、

[ソース]

1
test.head(10)

[結果]

keyword,location,text項目はありますが、target項目がありませんので、このデータを予測する必要があることが確認できました。

分布確認

targetごとのデータ数を分布図で確認してみます。

[ソース]

1
2
3
x=tweet.target.value_counts()
sns.barplot(x.index,x)
plt.gca().set_ylabel('samples')

[結果]

災害関連ツイート(=1)よりも災害に関連しないツイート(=0)の方が多いことが分かります。

次回からはツイート(text)に関するEDA(探索的データ解析)を行っていきます。

Kaggle - Digit Recognizer(5) - 手書きの数字認識

手書きの数字認識問題Digit Recognizerの5回目(最終)の記事になります。

今回は、予測結果をKaggleに提出します。

前回記事のTorchvisionで用意されているResNet-18(畳み込みニューラル ネットワーク)モデルを使った成績が一番よかったので、このソースに提出ファイルの作成処理を追加します。

提出ファイルの作成

まずKaggle環境からtest.csvファイルをダウンロードしておきます。

前回のソースに下記のソースを追記し、実行します。(処理内容はコメントをご参照ください。)

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test_data = pd.read_csv('./test.csv', dtype=np.float32)
test_data = test_data.to_numpy() / 255.0
test_data = test_data.reshape(-1, 28, 28, 1) # reshapeの一つのサイズが決まっているとき、もう一方を-1とすると、-1には元の形状から推測された値が入る。

test_tensor = torch.from_numpy(test_data).permute(0, 3, 1, 2) # numpy ndarrayからPytorch tensorに変換し、次元の入れ替え(permute)
test_tensor = test_tensor.repeat(1, 3, 1, 1) # データセットの繰り返し

images = test_tensor.to(device)
outputs = model(images)
_, predictions = torch.max(outputs, 1) # 配列の最大値の要素を返す

predictions = predictions.cpu()
submission = pd.DataFrame({'ImageId': np.arange(1, (predictions.size(0) + 1)), 'Label': predictions})
submission.to_csv("submission.csv", index = False)
print("# 終了 #")

submission.csvが出力されますので、このファイルをKaggleに提出します。

[提出結果]

提出結果は98.21%となりました。

十分な正解率になったかと思います。

Kaggle - Digit Recognizer(4) - 手書きの数字認識

手書きの数字認識問題Digit Recognizerの4回目の記事になります。

今回はTorchvisionで用意されているResNet-18(畳み込みニューラル ネットワーク)モデルを使って数字認識を行います。

(Torchvisionはpytorchが用意してくれている画像周りのDataLoaderです。)

Torchvisionで数字認識

最初はKaggleノートブックで実行していたのですが、Torchvisionのデータセットをロードするところでエラーになってしまい解消方法がみつかりませんでした。

そこで今回はローカルで実行しております。

必要ライブラリをpipインストールする必要がありましたが、基本的に’No Module’と表示されるものをインストールすれば問題ありませんでした。

またKaggle環境からtrain.csvをダウンロードしておく必要もあります。

処理に関してはソース上のコメント参照して頂ければと思います。

[全体ソース]

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# ライブラリをインポート
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os
from tqdm import tqdm
import seaborn as sns
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore")

import torch
import torch.nn as nn
import torchvision
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms

# CSVファイルを読み込み、正解ラベルのデータを分ける
data = pd.read_csv('./train.csv', dtype = np.float32)
labels = data.pop('label').astype('int64')

# 0~255のピクセルデータを0~1に変換
data = data.to_numpy() / 255.0 # converting to numpy and normalizing between 0 and 1
labels = labels.to_numpy()

data = data.reshape(-1, 28, 28, 1)
labels = labels.reshape(-1,1)
print(labels.shape)

# 訓練データと評価データに分ける
x_train, x_val, y_train, y_val = train_test_split(data, labels, test_size=0.2)
print(f'x_train.shape: {x_train.shape}, x_val.shape: {x_val.shape}')

# カスタムデータセットの定義
class MNISTDataset(Dataset):

def __init__(self, images, labels, transform = None):
"""Method to initilaize variables."""
self.images = images
self.labels = labels
self.transform = transform

def __getitem__(self, index):
label = self.labels[index]
image = self.images[index]

if self.transform is not None:
image = self.transform(image)
image = image.repeat(3, 1, 1)
return image, label

def __len__(self):
return len(self.images)

# データを0~1のテンソルに変換
train_set = MNISTDataset(x_train, y_train, transform=transforms.Compose([transforms.ToTensor()]))
val_set = MNISTDataset(x_val, y_val, transform=transforms.Compose([transforms.ToTensor()]))
all_data = MNISTDataset(data, labels, transform=transforms.Compose([transforms.ToTensor()]))

train_loader = DataLoader(train_set, batch_size=32)
val_loader = DataLoader(val_set, batch_size=32)
all_data_loader = DataLoader(all_data, batch_size=32)

# GPUが使えればGPUを使用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

learning_rate = 0.001
num_classes = 10
num_epochs = 10
# TorchvisionからResNet-18(畳み込みニューラル ネットワーク)モデルをダウンロード
model = torchvision.models.resnet18(pretrained=True) # Kaggleノートブックではここでダウンロードエラー
num_ftrs = model.fc.in_features
# 全結合を行う3層構造のニューラルネットワークを生成
model.fc = nn.Linear(num_ftrs, num_classes)

model.to(device)

criterion = nn.CrossEntropyLoss()

# 全てのパラメータが最適化されることを観察
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)

# 7エポックごとにLRを0.1ずつ減らす
# LR range test:初期学習率を決める手段で、ある幅で学習率を徐々に増加させながらAccuracyないしLossを観察し決定する手法
exp_lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)

# 損失関数と最適化アルゴリズムを生成
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

# 学習率を更新する
def update_lr(optimizer, lr):
for param_group in optimizer.param_groups:
param_group['lr'] = lr

# 学習する
total_step = len(train_loader)
curr_lr = learning_rate
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
images = images.to(device)
labels = labels.to(device)

# Forward pass(順伝搬:初期の入力を層ごとに処理して出力に向けて送ること)
outputs = model(images)
loss = criterion(outputs, labels.flatten())

# Backward and optimize(逆伝播と最適化を行う)
optimizer.zero_grad()
loss.backward()
optimizer.step()

if (i + 1) % 300 == 0:
print(f'Epoch: {epoch + 1}/{num_epochs}, Loss: {loss.item()}')

# 評価する
model.eval()
with torch.no_grad():
correct = 0
total = 0
for images, labels in val_loader:
images = images.to(device)
labels = labels.to(device)

outputs = model(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels.flatten()).sum()

print(f'Test acc: {100 * correct / total}')

[結果]

最終正解率は98.38%となりました。

前回記事の正解率は95%前後だったのでそれよりも精度を上げることができています。

数字認識の成果としては十分なものになっていると思います。

Kaggle - Digit Recognizer(3) - 手書きの数字認識

手書きの数字認識問題Digit Recognizerの3回目の記事になります。

今回は畳み込みニューラルネットワーク(Convolutional Neural Networks)の処理を実装していきます。

CNNは画像全体にわたって、特徴の一致件数を計算することによって、フィルタをかける手法です。この時に実行する演算が「畳み込み」で、畳み込みニューラルネットワークの名称はここから取られています。

画像や動画認識に広く使われているモデルです。

ライブラリのインポート

必要なライブラリをインポートします。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os
from tqdm import tqdm
from sklearn.model_selection import train_test_split
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")

import torch
import torch.nn as nn
from torch.autograd import Variable
import torchvision

CSVファイル読み込み・正解ラベルの準備

CSVファイルを読み込み、正解ラベルのデータを分けます。

正解ラベルはNumpyのint32型に変換します。

[ソース]

1
2
3
4
data = pd.read_csv('../input/digit-recognizer/train.csv', dtype = np.float32)
labels = data.pop('label').astype('int32')

data.head() # let's see first five rows

[結果]


変換した正解ラベルを表示します。

[ソース]

1
labels.head() # after converting labels to `int32`

[結果]


次に0~255のピクセルデータを0~1に変換します。(正規化)

正解ラベルと0~1に変換したデータはNumpyの配列型にしておきます。

また、訓練データと評価データに分けます。(4:1の割合)

[ソース]

1
2
3
4
5
data = data.to_numpy() / 255.0 # converting to numpy and normalizing between 0 and 1
labels = labels.to_numpy()

x_train, x_val, y_train, y_val = train_test_split(data, labels, test_size=0.2)
print(f'x_train.shape: {x_train.shape}, x_val.shape: {x_val.shape}')

[結果]

ピクセルデータをイメージ化

手書き数字のピクセルデータをイメージ化して表示します。

[ソース]

1
2
3
4
5
6
7
8
plt.figure(figsize=(12, 10))
for i in range(16):
plt.subplot(4, 4,i+1)
plt.xticks([])
plt.yticks([])
plt.imshow(x_train[i].reshape(28,28), cmap=plt.cm.binary)
plt.xlabel(y_train[i])
plt.show()

[結果]

PyTorchデータセットの準備

NumpyのデータをPyTorchのデータセットに変換します。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
x_train_tensor = torch.from_numpy(x_train)
y_train_tensor = torch.from_numpy(y_train).type(torch.LongTensor)

x_val_tensor = torch.from_numpy(x_val)
y_val_tensor = torch.from_numpy(y_val).type(torch.LongTensor)

print(f'x_train_tensor.dtype: {x_train_tensor.dtype}, y_train_tensor.dtype: {y_train_tensor.dtype}')

train_data = torch.utils.data.TensorDataset(x_train_tensor, y_train_tensor)
val_data = torch.utils.data.TensorDataset(x_val_tensor, y_val_tensor)

train_loader = torch.utils.data.DataLoader(dataset=train_data, batch_size=32, shuffle=True)
val_loader = torch.utils.data.DataLoader(dataset=val_data, batch_size=1, shuffle=False)

[結果]


GPUが使用できる場合は、GPU使用するように設定します。GPUがない場合はCPUを利用します。

[ソース]

1
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

CNNモデルの定義

CNNモデルを定義します。

損失関数(交差エントロピー)と最適化アルゴリズム(SGD Optimizer)もあわせて定義します。

[ソース]

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
learning_rate = 0.001
num_epochs = 10
num_classes = 10

# Convolutional neural network (two convolutional layers)
class ConvNet(nn.Module):
def __init__(self, num_classes=10):
super(ConvNet, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
nn.BatchNorm2d(16),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))

self.layer2 = nn.Sequential(
nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2))

self.fc = nn.Linear(7 * 7 * 32, num_classes)

def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
return out

model = ConvNet(num_classes).to(device) # CNNモデルの生成

criterion = nn.CrossEntropyLoss() # 交差エントロピーの生成
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate) # SGD Optimizer(確率的勾配降下法)の生成

学習と評価

CNNモデルを使って学習と評価を行います。

Kaggleのノートブック環境で実行しましたが30分以上かかりましたので気長にお待ちください。

[ソース]

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
iter_num = 0
loss_list = []
iteration_list = []
accuracy_list = []

# Train the model()
total_step = len(train_loader)
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader): # (訓練データごとにループ)
images = images.view(32, 1, 28, 28).to(device)
labels = labels.to(device)

# Forward pass(順伝搬:初期の入力を層ごとに処理して出力に向けて送ること)
outputs = model(images)
loss = criterion(outputs, labels)

# Backward and optimize(最適化、確率的勾配降下法)
optimizer.zero_grad()
loss.backward()
optimizer.step()

iter_num += 1

if iter_num % 50 == 0:
# Calculate Accuracy(正解率の算出)
correct = 0
total = 0
# Iterate through val dataset(評価データごとにループ)
for images, labels in val_loader:
test = images.view(1, 1, 28, 28).to(device)
labels = labels.to(device)
# Forward propagation(順伝搬:初期の入力を層ごとに処理して出力に向けて送ること)
outputs = model(test)

# Get predictions from the maximum value(予測値の取得)
predicted = torch.max(outputs.data, 1)[1]

# Total number of labels(正解ラベルの総数)
total += len(labels)

correct += (predicted == labels).sum()

accuracy = 100 * correct / float(total)

# store loss and iteration(損失・正解率を保持)
loss_list.append(loss.data)
iteration_list.append(iter_num)
accuracy_list.append(accuracy)
if iter_num % 500 == 0:
# Print Loss(損失と正解率を表示)
print('Iteration: {}, Loss: {:.4f}, Accuracy: {:.4f} %'.format(iter_num, loss.data, accuracy))

[結果]

最終的な正解率は94.98%になりました。


学習済みモデルを使って最終評価を行います。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Evaluation(評価)
correct = 0
total = 0
# Iterate through val dataset(データごとにループ)
for images, labels in val_loader:
test = images.view(1, 1, 28, 28).to(device)
labels = labels.to(device)
# Forward propagation(順伝搬:初期の入力を層ごとに処理して出力に向けて送ること)
outputs = model(test)

# Get predictions from the maximum value(予測値の取得)
predicted = torch.max(outputs.data, 1)[1]

# Total number of labels(正解ラベルの総数)
total += len(labels)

correct += (predicted == labels).sum()

accuracy = 100 * correct / float(total)
print(accuracy)

[結果]

同じく正解率は94.98%です。

グラフ化

損失と正解率の推移をグラフ化します。

[ソース]

1
2
3
4
5
6
7
8
9
10
11
12
13
# Visualize loss
plt.plot(iteration_list,loss_list)
plt.xlabel("Num. of Iters.")
plt.ylabel("Loss")
plt.title("Loss vs Num. of Iters.")
plt.show()

# Visualize Accuracy
plt.plot(iteration_list,accuracy_list)
plt.xlabel("Num. of Iters.")
plt.ylabel("Accuracy")
plt.title("Accuracyvs Num. of Iters.")
plt.show()

[結果]

前回記事(ロジスティック回帰モデル)と比べると損失はより早く収束しているように見えます。

また正解率に関しては収束のペースはあまり変わってないようですが、最終正解率は85%前後から95%前後と10%ほど精度が上がっています。