u++の備忘録

【Pandas】ある条件の列名の列を足し合わせる

業務で書いた。いろいろググったのでメモ。

要件

以下のようなテーブルがあったときに、2018年6月の合計を計算したい、つまり"201806"から始まる列の値を足し合わせたい。

user id 20180601 20180602 20180603 20180604 20180807
aaaaa 0 500 0 500 1000
aaaab 10 500 0 500 1000
aaaac 0 200 300 100 400

実装

df['201806'] = df.iloc[:,df.columns.str.startswith('201806')].sum(axis=1)

df.columns.str.startswith('201806')

  • df.columns で列名の一覧を出す
  • .str.startswith('201806') で、それぞれの列名が'201806'で始まるか否かを判定

df.iloc

  • 行や列を指定して取り出す

.sum(axis=1)

  • 列を足し合わせる

競技プログラミングを始めた&ABC104

競技プログラミングを始めました。「AIが〜」というオジサンではなく、きちんと自分の手を動かせる人間でありたいという気持ちです。

やったこと

環境構築

下記サイトを参考にしました。
VSCodeで始める競技プログラミング(環境構築編)

AtCoder Beginners Selection

下記記事を読みながら、問題を解きました。
qiita.com

AtCoder Beginner Contestの過去問を解く

いくつか過去問を解き、自分の現状を把握しました。


AtCoder Beginner Contest 104に参加

初コンテスト参加でしたが、AとBしか解けませんでした。参加者の反応を見るとC問題としては難しかったようですが、ABCの問題は基本的に全完できるようにするのが目標なので、良い教訓になりました。

AtCoder Beginner Contest 104 - AtCoder

今後

↑のツイートへの返信で、現状からステップアップするためには何か書籍で体系的に勉強した方が良さそうだとご教示いただきました。可能な限りコンテストには参加しつつ、書籍での勉強を進めていく予定です。

Jリーグの戦評、「1秒あまり」で自動作成 Jリーグ公式サイトからテキスト速報をスクレイピングして試合を要約する

はじめに

先日(2018年7月24日)公開された下記の記事を見て、自分でも似たものを作ってみたくなりました。

www.itmedia.co.jp

神戸新聞社が開発した「経過戦評ロボットくん」

www.kobe-np.co.jp

記事などから読み取れた情報を基に、こちらのプログラムのアルゴリズムを、以下に示します。

  1. 神戸新聞が運営する「兵庫の高校野球|神戸新聞NEXT」にて、リアルタイムに(手動で)一打席速報を掲載
  2. 試合終了を契機にプログラムが起動
  3. 一打席速報のデータを読み込む
  4. ひとつひとつの打席結果を分析し、事前に用意した「得点表」を基にそれぞれの得点シーンについて勝敗への影響度を算出
  5. 高い点数がついた得点シーンを複数組み合わせ、文章として違和感なくまとめる

事前に用意した「得点表」は、記者が執筆し神戸新聞に掲載されてきた試合の戦評を基に、機械学習で作成されたとのことです。

詳細は記事などに記載がなかったので私の推察になりますが、以下のような教師あり学習だと思います。

  1. 「一打席速報」の文章を単語単位に形態素解析して分かち書きし、Bag-of-words形式で特徴ベクトル Xにする
  2. それぞれの「一打席速報」の文章が「記者が執筆し神戸新聞に掲載されてきた試合の戦評」に採用されていれば目的変数 Yを1、されていなければ0にする
  3.  X Yのペアを訓練データとして、重回帰分析などで Y=1に寄与している Xの要素を特定する

若干想像しづらいと思うので、架空の具体例を用いて説明します。

とあるA高校とB高校の試合で得られた「一打席速報」
1回表 A高校 3番 XXX「ライトスタンドへのホームランが飛び出し、A高校が1点を先制」
3回表 A高校 9番 YYY 「二死ランナー無しで、サードへの内野フライ」
9回裏 B高校 4番 ZZZ 「レフトスタンドへの逆転サヨナラ満塁ホームラン」

A高校とB高校について、実際に掲載された戦評
1回表、A高校は3番XXXのライトスタンドへのソロホームランで先制。しかし9回裏にB高校は4番ZZZが起死回生の逆転サヨナラ満塁ホームランを放ち、4-1で勝利を収めた。

この場合、訓練データとなる X Yのペアは以下のようになります。ただし Xの数値はそれぞれ(ライト, スタンド,ホームラン, 先制, 二死, ランナー無し, サード, 内野フライ, レフト, 逆転, サヨナラ, 満塁)の登場回数です。この単語群は、一打席速報の文章から作成しています。

 X  Y
(1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0) 1
(0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0) 0
(0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1) 1

この訓練データを基に何かしらのアルゴリズム教師あり学習させることで、どの単語が含まれている「一打席速報」が、実際に「記者が執筆し神戸新聞に掲載されてきた試合の戦評」に入っているかが分かります。ここから、どの「一打席速報」を基にして戦評を作成すれば良いかを類推できるという仕組みです。

(今回の例で言うと「スタンド」「ホームラン」などが含まれる「一打席速報」は、戦評に含まれるべきと判定されます)

Jリーグ版を作った

読み取った情報を基に、Jリーグ版を作成しました。

github.com

生成した戦評

先に結果をお見せすると、例えばとあるJリーグの試合(浦和レッズVS川崎フロンターレ, 2018年8月1日(水)19:03KO)について、次のような戦評を生成できました。

[2-0でホームの浦和が勝利。J1通算400勝を達成した]
浦和は前半7分、岩波からのロングボールに武藤が抜け出す。持ち上がった武藤は右サイドの敵陣深くからグラウンダーで折り返すと、興梠が右足でチョンソンリョンの頭上を越えるループシュートを放ち、ゴールに吸い込まれる。浦和は後半47分、李が自陣からドリブルで持ち上がる。ペナルティエリア左に進入すると、鈴木にタックルで倒されてPKを獲得。キッカーはファブリシオ。チョンソンリョンの逆を突き、ゴール左に沈め、リードを広げる。2-0でホームの浦和が勝利。J1通算400勝を達成した。

アルゴリズムの概要

アルゴリズムの概要を以下に示します。いろいろ検討したのですが、サッカーの場合は野球ほど1試合当たり点数が入らないので、「経過戦評ロボットくん」のように機械学習で事前に「得点表」を用意する必要がない気がしました。ガバガバですが、ゴールが入ったプレー全てを戦評に含めるルールベースの実装になっています。

  1. Jリーグ公式サイトからプレーのテキスト速報をスクレイピング
  2. ゴールが入ったプレーのテキスト速報を基に、文言を微調整して戦評を作成
テキスト速報のスクレイピング

以下のURLをプログラムにコマンドライン引数として渡すことで、サイトからテキスト速報をスクレイピングします。

www.jleague.jp

スクレイピングには、seleniumを利用しています。最初はbeautifulsoup4を使おうと思いましたが、上記のサイトはテキスト速報部分を別途動的に読み込む仕様になっていたのでブラウザ経由でhtmlを取得できるseleniumを採用しました。

ゴールが入ったプレーか否かの判定

ゴールが入ったプレーのテキスト速報には、下図のように「GOOOOOOOOOOAL!」という画像が挿入されていました。

f:id:upura:20180805153153p:plain

このことを活かし、画像タグが挿入されているか否かで判断をするロジックを組みました。

    # ゴール判定
    goal_list = driver.find_elements_by_class_name('spotRightTxt')    # クラス名'spotRightTxt'の全要素をリスト形式で取得
    goals = [1 if '<img class=' in goal.get_attribute("innerHTML") else 0 for goal in goal_list]    # 要素に画像タグがあれば1, なければ0のリスト内包表記
文言を微調整して戦評を作成

テキスト速報の末尾には句点がなかったり、PKの場合にはPKが発生した文章も戦評に含めたかったりと、今回の要件に沿うように戦評を作成しました。

おわりに

より高度な戦評を作成する場合、機械学習を用いた「得点表」の作成も選択肢に入ってくるかと思います。例えば分量の制限があり全得点については掲載できない場合や、重要な選手交代やファールについて取り上げる場合なども想定できます。こちらは訓練データを作成するのが大変だと思うので、もしやる気が出たらやってみようと思います。

【論文メモ】プライバシーを守るDeep Neural Network 暗号化したMNISTでも分類性能97%

2P-DNN : Privacy-Preserving Deep Neural Networks Based on Homomorphic Cryptosystem

概要

Microsoft Azure, Amazon AWSといったMachine Learning as a Service (MLaaS)の利便性が増しているが、ユーザのプライバシー保護の観点からMLaaSのサーバへのアップロードがはばかられる場合も多く、プライバシー保護のため暗号化されたデータを用いるとDNNモデルの性能を十分に発揮できない。

本研究では、"Paillier homomorphic cryptosystem"という暗号化の仕組みを用いた Privacy-Preserving Deep Neural Networks (2P-DNN) を提案。下図のように暗号化したMNISTデータセットでの分類性能が97%を示し、既存のプライバシー保護DNNよりも高い性能を発揮した。

https://user-images.githubusercontent.com/31459778/43379567-1c5b50fc-9408-11e8-8890-b88dbc46daa0.png

所感

MLaaSは大変便利だが、会社の制約として顧客情報を他社サーバにアップロードできないのが一般的な現状だと思う。その現状を鑑みて、暗号化したデータセットでも高い性能を発揮できる仕組みは需要が大きい気がする。

25歳になりました

本日、下記の記事でも書いた通り、25歳の誕生日を迎えました。なんかキリの良い数字な気がするので、直近1年の振り返りと今年の目標をつらつらと書きます。

upura.hatenablog.com

f:id:upura:20180725174942j:plain
↑お祝いで食べたケーキ(の代わり)

24歳の1年でやったこと

大学院を退学した

去年の9月末に大学院を辞めました。

upura.hatenablog.com

・社会人になった

10月1日付けで企業に正社員として入社しました。主にSQL, Python, Rを用いて、営業・マーケティング向けデータ分析を担当しています。概ね満足しています。

upura.hatenablog.com

・食生活管理と運動・筋トレを継続的に

相変わらずお酒は好きですが、アラサーが見えてきたので暴飲暴食を控える意識をしています。あと定期的なウォーキング・ジョギング・筋トレを継続しています。

・Kaggle・競技プログラミングを始めた

AIが相変わらず流行している今だからこそ、きちんと自分で手を動かす力を身に付けたいと思って始めました。ぼちぼちやっていきたいです。

今年の目標

1年後など想像しても仕方ないので、ざっくりとした目標だけ。25歳も精進していきます。

・勉強をする

心までおじさんにならないよう、常に学び続けたいです。

・アウトプットをする

twitterやブログ更新や対外発表など、定期的にしていきたいです。インプットとアウトプットのバランスの良さを大切にしたいです。

・自分を労る

いろいろ無理がきかなくなってくる歳だと思うので、精神的にも身体的にも健康的に生きていきたいです。

pythonのunittestでコマンドライン引数をテストする方法と注意点

以下の記事で作ったプログラムにおいて、コマンドライン引数をテストするケースがありました。少なくとも日本語ではまともな記事が存在しなかったので、本記事ではその手法をまとめます。

upura.hatenablog.com

テストする関数

実行時に与えたコマンドライン引数(一つ目)を取得し、'Happy birthday, ' + str(コマンドライン引数) + '!' という文字列を返します。ちなみに、sys.argv[0] には実行したファイル名が格納されています。

happy.birthday.ga/happy_birthday_ga.py at master · upura/happy.birthday.ga · GitHub

def get_objective_sentence():
    try:
        USER_NAME = sys.argv[1]
    except:
        USER_NAME = 'upura'

    objective_sentence = 'Happy birthday, ' + str(USER_NAME) + '!'
    return objective_sentence

テストコード

happy.birthday.ga/test_get_objective_sentence.py at master · upura/happy.birthday.ga · GitHub

import unittest
import sys, os
sys.path.append(os.getcwd())
from happy_birthday_ga import get_objective_sentence


class TestGetObjectiveSentence(unittest.TestCase):
    # User name is not given
    def test_success_get_default(self):
        expected = get_objective_sentence()
        actual = 'Happy birthday, upura!'
        self.assertEqual(expected, actual)

    # User name is given
    def test_success_get_user_name(self):
        sys.argv.append('Shinzo Abe')
        expected = get_objective_sentence()
        actual = 'Happy birthday, Shinzo Abe!'
        self.assertEqual(expected, actual)
        del sys.argv[1]

    # User name is given (Not a string)
    def test_success_get_not_string(self):
        sys.argv.append(111)
        expected = get_objective_sentence()
        actual = 'Happy birthday, 111!'
        self.assertEqual(expected, actual)
        del sys.argv[1]

if __name__ == "__main__":
    unittest.main()

解説

コマンドライン引数の与え方

コマンドライン引数はsys.argvにリスト形式で格納されるので、下記のように擬似的にコマンドライン引数を与えることができます。

sys.argv.append('Shinzo Abe')

コマンドライン引数のリセット

以下のように都度コマンドライン引数をリセットしないと、テストケースが複数ある際に以前のコマンドライン引数が蓄積されていってしまうので注意が必要です。

del sys.argv[1]

引数を複数与える場合は del sys.argv[1:] で削除すれば良いです。

遺伝的アルゴリズムでAIに自分の誕生日を祝ってもらう

突然ですが、本日7月25日は僕の誕生日です。

とはいえ、特に誰かが祝ってくれるわけでもないので「無いなら作る」というエンジニア精神で、誕生日を祝ってくれるプログラムを実装しました。

GitHub
github.com

システム要件

  1. コマンドライン引数で、祝ってもらう人(今回は自分)の名前を渡せるようにする
  2. プログラムが「試行錯誤」しながら"Happy birthday"と誕生日を祝ってくれる

システムの実装

[要件1] コマンドライン引数で名前を渡す

まず要件1について、プログラムに出力してもらう文言をユーザがコマンドライン引数で設定できるようにします(参考 *1)。

例えば下記の test_argv.py を次のようにターミナルで実行します。

test_argv.py

import sys
 
args = sys.argv
 
print(args)
print('第1引数:' + args[1])

ターミナル

python test_argv.py 'upura'

すると、ターミナルのコマンドライン引数として渡した'upura'が sys.argv にリスト形式で格納されます。

ターミナルの出力

['test_argv.py', 'upura']
第1引数:upura

この仕組みを応用することで、コマンドライン引数で受け取った名前を含んだ文章を出力できるよう実装します。

今回は 'Happy Birthday, (コマンドライン引数)!' という文章を、目標の出力とします。以下の関数でコマンドライン引数を取得しました。

def get_objective_sentence():
    try:
        USER_NAME = sys.argv[1]
    except:
        USER_NAME = 'upura'

    objective_sentence = 'Happy birthday, ' + str(USER_NAME) + '!'
    return objective_sentence

この部分のunittestについては、別記事にまとめました。
upura.hatenablog.com

[要件2] 遺伝的アルゴリズムの活用

次いで要件2のため、今回は「遺伝的アルゴリズム」を用いて、目標の文章を出力する仕組みを作りたいと思います。

遺伝的アルゴリズムとは、生物界の進化の仕組みを模倣する解探索手法です*2。大雑把に言うと、ランダムに大量の解候補を生成し、たまたま「良くできた」候補を元に次なる候補を生成していくことで、目標の解にたどり着こうとする手法です。

今回の手順の概要を以下の図に示します。

f:id:upura:20180723200505p:plain

下記の手順で、目標の文章を出力する仕組みを実装しました。

  1. 「現世代」の N個の個体をランダムに生成する
  2. 「現世代」の各個体の「適応度」をそれぞれ計算する
  3. ある確率で次のいずれかの遺伝子操作を実施し、結果を「次世代」に保存する
    1. 個体を二つ選択して「交叉」
    2. 個体を一つ選択して「突然変異」
    3. 個体を一つ選択して「複製」
  4. 「次世代」の個体数が N個になるまで上記の動作を繰り返す
  5. 「次世代」の個体数が N個になったら「次世代」を「現世代」と見なす
  6. 2以降の動作を、目標の解を導出できる個体が出現するまで繰り返す

ただし N=1000、遺伝子操作の選択確率は (交叉, 突然変異, 複製)=(0.1, 0.1, 0.8)としています。

以下では、各ステップについて説明していきます。

1. 「現世代」の N個の個体をランダムに生成する

ここでは、以下の関数で目標の文章と同じ長さの文章を生成します。単語の候補はアルファードの大文字・小文字に加え、半角スペース・半角カンマ・半角感嘆符です。

例えば`p!VGtdbirQedHyLtjaLfr!`のような文章を一つの個体として生成します。

def get_string_candidate():
    string_candidate = \
        [chr(i) for i in range(97, 97 + 26)] + [chr(i) for i in range(65, 65 + 26)]    
    string_candidate.append(' ')
    string_candidate.append(',')
    string_candidate.append('!')
    return string_candidate

def get_sentence(string_candidate, sentence_length):
    created_sentence = random.choices(string_candidate, k = sentence_length)
    return created_sentence
2. 「現世代」の各個体の「適応度」をそれぞれ計算する

ここでは、各個体の「適応度」を以下の関数で計算します。純粋に一文字単位で目標の文章との差異を調べています。

例えば'p!VGtdbirQedHyLtjaLfr!'の場合は、太字の部分が目標の文章と一致しているので、適応度は(一致した文字数)/(全文字数)として計算されます。

def get_fitness(objective_sentence, sentence):
    judgment = [i == j for i, j in zip(objective_sentence, sentence)]
    evaluation = sum(judgment) / len(judgment)
    return evaluation
3. ある確率で次のいずれかの遺伝子操作を実施し、結果を「次世代」に保存する

ここでは、以下のいずれかの遺伝子操作を実施します。

ただし以下の遺伝子操作における「選択」には「ルーレット選択」と呼ばれる手法を採用しました。ルーレット選択は個体 iの適応度を f_iとしたときに、個体 iを選ぶ確率 p_iを下記のように計算する手法です。

 {\displaystyle p_{i}={\frac {f_{i}}{\sum _{k=1}^{N}f_{k}}}}

def fitness_proportionate_selection(agents, evaluations):
    weight = np.array(evaluations) / sum(evaluations)
    index = np.arange(AGENT_NUM)
    return agents[np.random.choice(index, p = weight)]

個体を二つ選択して「交叉」

ここでは、「二点交叉」という手法を採用しました。交叉点をランダムで二つ選び、二つの交叉点に挟まれている部分を入れ換える方式です。

def crossover(agents, evaluations):
    father_agent = fitness_proportionate_selection(agents, evaluations)
    mother_agent = fitness_proportionate_selection(agents, evaluations)
    crossover_points = random.sample(list(np.arange(len(father_agent) - 1)), 2)
    next_agent = father_agent[:min(crossover_points)] \
        + mother_agent[min(crossover_points):max(crossover_points)] \
        + father_agent[max(crossover_points):]
    return next_agent

個体を一つ選択して「突然変異」

ここでは、個体の各要素を0.1の確率で別の単語に置き換えます。

def mutation(agents, evaluations, string_candidate):
    selectd_agent = fitness_proportionate_selection(agents, evaluations)
    next_agent = [i if random.random() > 0.1 else \
        get_sentence(string_candidate, 1)[0] for i in selectd_agent]
    return next_agent

個体を一つ選択して「複製」

ここでは、選択された個体をそのまま「次世代」に引き継ぎます。

def reproduction(agents, evaluations):
    return fitness_proportionate_selection(agents, evaluations)
4. 「次世代」の個体数が N個になるまで上記の動作を繰り返す

ここでは、3の操作を N回繰り返します。

5. 「次世代」の個体数が N個になったら「次世代」を「現世代」と見なす

ここでは、現時点での「次世代」を「現世代」と見なします。

6. 2以降の動作を、目標の解を導出できる個体が出現するまで繰り返す

ここでは、2以降の動作を繰り返し、目標の解を導出できる個体が出現するまで新たなる「次世代」を生成していきます。

デモンストレーション

以下は、各世代で一番適応度の高い個体を出力しています。徐々に目標の文章に近づいていっている様子が分かります。

Gif

f:id:upura:20180721165641g:plain

最終出力結果

最後には、目標の文章を出力できています。277世代までかかったので、約277000の個体を生成したことになります。

f:id:upura:20180721165800p:plain

終わりに

今回は、遺伝的アルゴリズムを用いて、自分の誕生日を祝ってくれるプログラムを実装しました。プログラムはGitHubで公開しており、簡単にですがREADMEやユニットテストも整備しています。気が向いたら自分の名前を入れて遊んでみてください。
github.com

誕生日のお祝いとして、はてなブックマークSNS拡散も大歓迎です!!!