赤羽でひとり食べ歩く

最近は電車で通勤するのはもっぱらミーティングのある水曜日だけで,それ以外は車で職場まで通っている.車で来ていて辛いのは,うわー疲れた,という日でも,ともかく家まで運転して帰らないといけないこと.お腹が空いていても都内では駐車場のある飲食店は少ないし,あってもファミレスやファーストフードなどのチェーン店が多くて,一人でわざわざ入ろうという気にはならない.そして,お酒はもちろん飲めない.これは辛い.
そういうわけで,食べて帰ることがあるのは水曜日だけなのだけど,そういうとき都合が良いのが赤羽という街.埼玉に帰る場合,新宿からの湘南新宿ラインに加えて上野からの高崎線があるので帰りはすぐ電車が来るし,街の規模が小さいので駅から歩ける範囲で店がたくさんある.何より良いのは,一人で気楽に入れるこじんまりとした店がとても多いこと.自分ルールで,新宿駅湘南新宿ラインに乗れなかった場合や,乗れても赤羽までで座れなかった場合は降りて食べて(飲んで)帰ることにしている.今日は バーワルチー立ち飲みいこいと回った.備忘録代わりに最近行った店をいくつかメモしておく.

  • バーワルチー / インド料理: 21:00 過ぎに訪問.わずか5席のインド料理店.カレー2つ+ナンのBセット (950円) を注文.マトンのカレーとほうれん草のカレーを選択.マトンはサラサラ,ほうれん草はドロドロで良い対比になっていて,マサラもよく効いて美味しい.特に,ほうれん草のカレーはそのヘドロっぷりにほうれん草を強く感じられて,とても美味しかった.ナンは薄めでパリパリ.腹にたまらなくて良い.以前,夏に食べたくなる料理って少ないよね(冷やし中華ぐらい?)という話を学生としたのだけど,食べながら夏はカレーだなぁと思った*1.また,これだけ狭いと店主との距離も近く,デリー出身の店主とインド・ネパールの話などをしてしばし歓談.
  • 夏海 / ラーメン・つけ麺: 夜にラーメンを食べる場合はだいたいここ.全体的に高いレベルでまとまっている.接客も良い.赤羽はラーメン屋も結構充実していて,昼なら高はし(安くて美味しいがスープやや温めで基本並んでいる)もあるし,夜遅くても大勝軒まるいち 赤羽店があるなど選択肢に事欠かない.ちなみに,自家製麺 伊藤は食事としては物足りず,自家製麺 ほうきぼし(汁無し担々麺)は遠い(し,辛味が勝ち過ぎていて旨味が足りない*2)と感じる.
  • 立ち飲みいこい 本店 / 居酒屋: 赤羽が誇る立ち飲みの名店.何度か前は通ったけど,混んでいたり,ネクタイのサラリーマンが多かったりとお呼びじゃない感じがしていて一人では入りづらかった.閉店間際に入って,生ビール (350円) ともつ煮込み (110円) を頂いた.ビールはやや温めだったがもつ煮込みは抜群にうまい.安く,量も一人客向けに抑えられていて良い.もつ煮込みは好きで色んな所で頼むのだけど,美味しいと思ったのは永井食堂@渋川以来かもしれない.他のつまみも100-150円がほとんど.素晴らしい.ただし,喫煙可で,みなタバコを吸いまくっているし,床にはタバコが落ちまくっている.こもらないのであまり気にはならないが.
  • 鯉とうなぎのまるます家 総本店 / 居酒屋: 水曜に職場に泊まって,木曜の午前中に(職場が夏期閉室で)家に帰ったときに,途中で寄ってみた.日中だというのに,ランチを食べていた自分以外のほぼ全員が飲んでいて,かつ客層も40代後半から60歳代の一人客がメインと場違い感が半端無かった.自分など「僕ちゃん」と呼ばれそうな雰囲気.頼んだのは牛すじ煮込みのランチ.結構なボリュームで味はまあまあ.看板メニューの一つと思われるうな丼は,孤独のグルメでの訪問時の2倍 (1500円) まで値段が上がっている.夜に来てゆるりと飲むのが良いのでしょうね.
  • まるよし / 居酒屋: 他の居酒屋より少し遅く,23:00 まで遅くまで空いている居酒屋.もつ煮込み(小)が200円とか,200円代のメニューが多く,居酒屋としてはかなり安い部類だと思う(流石に立ち飲み屋にはかなわないが).内臓中心の焼き鳥が1本(80円)から頼めるのはとても有難い.しいたけや長葱を頼もうとしたら,売り切れと言われてその代わりに勧められたピーマン(+味噌だれ)がなかなか美味しかった.

家に着いたら,この間採録された国際会議のカメラレディ原稿の編集案(8P -> 4P にするにあたってどこを削るか)が学生から来ていたので軽くコメント.酒を飲んで仕事する趣味はないが,こういう論文でバッサリ削ったり(逆に大雑把に書いたり)といった作業は多少酒が入っていた方が思い切り良く(細かいことに拘らず)進められたりするのでたまに(意識的に)飲んでからやることがある.
そして,家では孤独のグルメ

*1:実は昼もBERG@新宿でカレーを食べていて,店に入った所で思い出して一瞬後悔したのだけど,全然違う方向性の味だったので十分満足できた.BERG の五穀米の十種野菜のカレー (504円) も早くて安くて健康的で乗り換え時にはおすすめ.

*2:汁無し坦々麺はそこらじゅうで食べているので要求する味のレベルが高くなってしまっている.旨味では池袋・十条の中国家庭料理 楊が,麻味(花椒)では板橋・池袋・本郷 栄児 家庭料理が,辣味(唐辛子)では戸田の麺や 双六がおすすめ.全体のバランスは神保町の担々麺本舗 辣椒漢が良い.担々麺では最近だと越谷中華キッチン ぐらが良かった.

共著論文が(ようやく)国際会議に採録された

指導している学生の論文がようやく国際会議に通ったようだ.博士課程の学生なので論文を書く地力がつくよう,大幅な書き直しが必要なときは(安易に直したりせず)何がまずいかを指摘するだけにして,なるべく学生自身に修正方法を考えてもらうようにしていたので,なかなか論文の完成度が上がらず,加えてプログラム・実験設定の不備が見つかることも多々あったりして,採録までに思いのほか時間がかかってしまった.まあ,論文を学会に通すことを目的として研究(指導)しているわけではないので良いのだけど,指導する側も投稿を続けることに疲れが出始めた頃だったので,通ってくれてホッとした.
今回の学会もちょっと今の論文の完成度では厳しいかな,と思っていたのだけど,2nd tier の会議で採択率が高いのと,プログラム委員が(恐らく)ポスター発表でなるべく多くの論文を拾うという方針だったらしい?*1のとが重なって,何とか拾ってもらえたようだ.学生にとっては初の査読付き国際会議の論文ということで,通ったのはもちろんめでたいけど,論文のページ数が半減して full paper ではなくなってしまったので,これで満足して欲しくないなぁ,というのが正直なところ.そういうわけで,最初にどう言葉をかけるか少し迷った(結局,おめでとう,良かったね,と言った).
博士課程の途中から指導するようになって,指導する側としても色々と試行錯誤するところがあり,難しいのだけど,学生自身が研究をすごく楽しんでやってくれているのが何よりの救いかな.修士号はともかく,博士号はやはり地力をしっかりつけて取らないと,出たあとで自分が望むような形で生きていけなかったりするので,効率が悪いようでも地道に積み重ねていってもらうしかないのかなと思う*2
今日は 22:30 ぐらいに出るつもりだったが,ある学生が詰パラを机の上に置いていたので,しばし詰棋談義に花を咲かせるなどして気がついたら 23:00 近くになっていた.詰将棋作者の話や,過去の良作の話をしてみたり.df-pn は実装が難しい,という話を聞くなど.しかし,まさか研究室でこんな話ができると思わなかったな.盛り上がり過ぎて,仕事中の別の学生に迷惑だったかもしれない.人工知能分野の問題として,指将棋も詰将棋(解図)も分かりやすい到達目標がなくなりつつあるという話は以前したけど,詰将棋(創作)はまだまだやる余地は残っていそう.評価データは詰パラとか50年分ぐらいあるし,誰かやらないかな.

*1:査読者三人中一人でも採録側に振れていればポスター論文として採録を考慮してくれていそうな感じだった・・・と思ったが,ポスター論文の採録本数は思っていたよりは少なそうな感じ.

*2:博士号を取った学生,取れなかった(取らなかった)学生のことを想うと,取るべくして取って欲しいと感じる.

深夜に職場から家まで車で帰ってみた

今日は車で職場まで来たのだけど,昼の都内はなかなかに混んでいてうんざりしたので,帰りは道が空いている深夜に帰ることにした.某国際学会の査読を3本ほど(残りは1本)片付けて,1:25 に職場を出発.環七通りに入ると,半分以上がタクシー.立体交差が多く信号の数が抑えられているので,制限速度一杯まで出してスイスイ走れる.ほとんどの車が制限速度をオーバーして走っているのでやや危なっかしい.
R254(川越街道)に左折して,すぐ R17(新大宮バイパス)に入る.トラックの割合が増えてきて,乗用車は半分ぐらい.新大宮バイパスも立体交差が多くて信号の数が抑えられているので,制限速度一杯まで出してスイスイ走れた.途中,夜食でもと思って,L.O. 3:00 のらーめん しおの風@南与野に立ち寄ってみたのだけど,閉まっていた.ラーメンデータベース によれば L.O. 22:30 とのこと.ひどい.仕方なく吉野家で鰻丼+Cセット.この間で20分ほどロスしてR17に戻り,上尾道路に入る.上尾道路は片側1車線の上,信号の切り替わりのタイミングが悪く,引っかかりまくり.そのまま終点まで走って最終的に家に着いたのは 3:05.職場からは 1h40m だけど,走っていた時間だけで言うと 1h20m ぐらいで,電車で通うよりはだいぶん早いようだ.距離は50km弱.
車で職場に来ると,終電を気にしなくても済むのでいつまでも仕事し続けることができるのだけど,あまりに遅くなると帰りの運転中で眠くなって危うい.かと言って,早く帰ろうとすると道が混んでいるので,別の意味で気を使うし疲れる.快適に帰るには,遅過ぎず,かつ程良く空いている時間を見極める必要がありそうだ.いずれにせよ,50km というのは毎日通勤したい距離ではないなぁ.毎日車で通うなら,通勤時間は1時間ぐらいまでかな.距離で言うと(東京都区部を通る場合)30km まで,地方なら 40km ぐらい.[追記] ラッシュ時の渋滞を甘く見ていた.都内のラッシュ時だと,1時間走っても20kmぐらいしか走れなさそう.自転車より遅かったり.
う〜ん,眠い.ひと通り読んで,採否だけ決めて,だらだらして6時頃睡眠.
[追記; 6/18] 懲りずに今日も車で職場に行ってみた.昼に川越街道〜笹目通り環八通りと通ったら,笹目通りに曲がる所で大渋滞.曲がったら工事中でさもありなん.その後も一箇所で工事中で,また渋滞.幹線道路は基本的に交通量が多いので(二車線で)一車線塞がると致命的に渋滞してしまう.これなら曲がる回数が多くても裏道を通り続けるのが良さそう.
帰りは,17:25 頃出て先日深夜に通った道を途中まで辿ってみたが,環七はひどい渋滞,川越街道はさほどでもなかったが,R17 に曲がって新大宮バイパスに入った辺りから慢性的な渋滞.1時間走って17kmしか進めず,東京から脱出もできず.まあ予想通りだけど,ひどい.これなら自転車の方がよっぽど早いで.1h20m 走っても,浦和にすら到達しなかった.深夜の2倍ぐらいかかっている.やはり,もっと遅くまでいる方が良いか.次は二分探索で 22:30 ぐらいに出てみよう.
首都圏の下道を2日連続で100km走るのは辛かった(特に今日は睡眠3時間だったので).しかし,

などを見ていると,片道120km通勤とかいう人がいる(高速ありっぽいが).かかっている時間を見る限り,下道のみで東京に長距離通勤している人は少なそうだけれど,皆さんよくやるなあ.
[追記; 6/20] 昨日電車で帰ったら座れず駅からの歩きもしんどかったので,また車で職場に来てみた.帰りは 22:30 頃出てみたら,かまだ渋滞しているふうだったので,夜食を食べて*1時間を潰すなどして仕切り直して 0:00 頃東京を出たら,スイスイ走れて快適に家まで帰れた.やはり,満員電車でしんどい思いして帰るぐらいなら,車で帰った方が全然いいなぁ.今回は,R17 から上尾道路に入らず R16 経由で県道215号に入ったら,信号もほとんど引っかからず早かった.覚えておこう.
[追記; 6/24] 0:30 に出て,環七 -> 川越街道 -> R17 -> R16 -> 県道215号と回って帰ったら 45km で 1h10m ほど.高速使わないのだとこの辺りが限界そうだ.まあ,何で通勤しても疲れるのには変わりない.自分のように特段の事情があって,やむを得ず遠くに住んでいるのでない限り,職場には限りなく近いところに住むのがよいと思う.
[追記; 7/8] 週3ぐらいで通っているが,慣れると案外普通に通える.先週金曜は 11:15 に出たらだいたい流れていてすいーっと帰れた.今日の帰りは 10:30 前に出て,最短ルートを追求して走ったら 1h12m ほどで帰れた.練馬の辺りは素直に環七から直接 R254 に入るのが良さそうだ.
[追記; 7/23] 21:00 頃でたが,都内は思いの外すいていてスイスイ.新大宮バイパスが少し混んでいたが流れていて,家まで 1h20m ぐらいだった.これぐらいならストレスないなあ.もう少し早い時間だとどうだろうか.

*1:環八沿いのみそ一発2@千歳船橋に行ってみたのだけど,進行方向反対車線だったもので,途中で遠回りしたり曲がり損ねたりしてえらい行きにくかった.駐車場があるという理由だけで選んだ店だったが,果たしてごく普通の野菜多めの味噌ラーメンだった.結局色々調べて分かったのは,職場近辺にはわざわざ車で行きたくなるような飲食店はない,ということだった.

結局,直帰するのが正解だった

昼過ぎに職場に着いたら部屋には誰一人いなくて,どこかいつも少し違う感じがする一日だった.神保町近辺で某論文誌の編集会議があったので,少し早めに出て二年ぶりに珈琲舎 蔵に寄った.今回は煙害もなく,客も少なかったので,査読をしながらブラジルとモカを一杯ずつ.チーズケーキも食べたのだけど,どうも意識が論文の方に行ってしまって珈琲自体をあまり味わうことができなかった.うーん,勿体無い.店で珈琲を飲むときは,何をしているのが良いのか.猿楽珈琲@恵比寿*1に行ったときのように,詰将棋でも解いているのが良いのかな.
某編集会議は予定の半分弱の時間で終わって,家に直帰するか職場に戻るか少し迷ったが,帰るのには早い時間だったのと珈琲豆を忘れていたので,結局職場に戻った.その後,博士学生たちと論文の方針について議論したりして一仕事.ついで,最寄りの沖縄料理店で同僚氏たちと一杯ひっかけて終電で帰ったのだけど,金曜の終電はぎゅうぎゅうのぎゅうで最後まで座れず,立ちっぱなし.満員電車で前にいた女性が体調が悪くなったのか車内で座り込んでしまっていたが,その気持はよく分かる.飲んだ後にあれは辛い.家に着いてから気がついたけど,珈琲豆は職場に忘れたまま.なんのために戻ったの.
論文投稿やミーティングでの発表があって忙しかった先週と比べれば今週はデモぐらいで比較的のんびりした一週間だったけど,雨続きで憂鬱でいまいち仕事が進まなかった.振り返ってみると,自分の研究室の昨年度の成果登録や学会出張の準備*2ぐらいしかしていなかったような気がする.来週から月末にかけて,査読の締め切りや新しい査読の依頼や論文投稿やミーティングなどなど,仕事がしだいに増えていくので,本当はもっと仕事を先取りしてしておかなければいけなかったのだけど.
出張準備の方は日程がお盆直前だけあって目ぼしい便は空席がどんどんなくなっていたので,チケットを予約するのにまず手間取った.そんな中やっと便を決めてオンラインで空席を確認してから予約を入れたら,空席はいまなくなったから別の便を選べと言われて予約ができなかった.すぐに別の便を探す気にもなれなかったので,その後1時間ぐらい放っておいたら3000円ほど高い料金で同じ便に一席だけ空きが出ていたので再度トライしたら予約できた.何なのこれ.その後,学会の Registration をして,ホテルを学会のサイトから予約しようとしたら,もう最寄りのところは空きがあまりなくてげんなり.学会のサイトからホテルを予約するのって(部屋の確保数が少なかったり,予約が集中したりで)良いところが予約できた試しがない.結局(空きがなかった)ホテルを HIS で探したら,まだ予約できた(上に,学会の割引料金よりだいぶ安かった)のでそちらで予約.
学会出張って,なんでいつもいつも7月とか8月とかハイシーズンにやるのかな.6月か9,10月ぐらいにやってくれれば余裕があって良いのに.行かないのもストレスだけど,行くのは行くのでしんどい.
[追記]予約した航空券,今日見てみたら1万5000円ぐらい値下がりしていた.えー.出発日が近づくにつれどんどん値上がっていくものだと思い込んでいたが,必ずしもそういうわけでもないのか.

*1:名刺の100円引きはまだ有効で,次回もまた使ってくださいと言われた.

*2:専門分野の最高峰の国際会議に共著論文が通った - ny23の日記で論文が採録された会議.行くか行かないかずいぶん迷ったが,最終的に本会議のみ参加することにした.自分が発表するかどうかは未定.というか口頭発表かポスター発表かもまだ決まっていない.

非線形分類器のミニバッチ分散オンライン学習器を実装してみた

ソフトウェアをいろいろ更新した - ny23の日記 で紹介した,

がようやく学会で発表されたようなので,非線形学習版の実装を置いておく.RBF カーネルを用いたオンライン学習器をミニバッチ+並列化したもの.実装したのはもう二ヶ月ぐらい前(論文の pdf を著者が公開した直後)なのだけど,流石に学会発表前に実装を出すのは気が引けたので公開を先延ばしにしていた.

// mpa1.cc ; GNU GPL version 2 copyright@id:ny23
#include <err.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <vector>
#include <numeric>
#include <map>
#include <algorithm>
#include <chrono>
#include <omp.h>

static const size_t MAX_LINE_LEN = 1 << 20;

typedef std::vector <std::pair <long, double> >  x_t;
template <typename T> using ex_base_t = std::pair <T, x_t>;
typedef ex_base_t <int>     ex_t;
typedef ex_base_t <double>  sv_t;
typedef std::vector <std::vector <std::pair <size_t, double> > >  f2ss_t;
struct result_t { int pp, pn, np, nn; };

// RBF kernel: exp (-gamma * |x-s|^2); assuming |s|^2 = |x|^2= 1
// note: linear kernel for negative gamma value
inline double kernel (const double dot, const double gamma)
{ return gamma < 0 ? dot : std::exp (-gamma * (2.0 - 2 * dot)); }

// compute margin for given example
double margin (const x_t& x, const f2ss_t& f2ss, const std::vector <double>& alph, const double gamma) {
  double m = 0.0;
  std::vector <double> dot (alph.size (), 0);
  for (const auto& f : x) {
    if (f.first >= static_cast <long> (f2ss.size ())) break;
    for (const auto& s : f2ss[f.first])
      dot[s.first] += f.second * s.second;
  }
  for (size_t i = 0; i < dot.size (); ++i)
    m += alph[i] * kernel (dot[i], gamma), dot[i] = 0.0;
  return m;
}

// read example from line and return label
int read_example (char*& p, x_t& x) {
  int y = static_cast <int> (std::strtol (p, &p, 10));
  double norm = 0.0;
  while (*++p && *p != '\n') {
    const long   i = std::strtol (p, &p, 10); ++p;
    const double v = std::strtod (p, &p);
    x.emplace_back (i, v);
    norm += v * v;
  }
  norm = std::sqrt (norm);
  for (auto& fn : x) fn.second /= norm;
  return y;
}

// read examples
std::vector <ex_t> read_data (const char* train) {
  char line[MAX_LINE_LEN];
  FILE* reader = std::fopen (train, "r");
  std::vector <ex_t> ex;
  for (x_t x; std::fgets (line, MAX_LINE_LEN, reader) != NULL; x.clear ()) {
    char* p = line;
    const int y = read_example (p, x);
    ex.emplace_back (y, x);
  }
  std::fclose (reader);
  return ex;
}

#ifndef USE_PERCEPTRON
double inner_product (const x_t& xi, const x_t& xj) {
  double dot = 0;
  auto it (xi.begin ()), jt (xj.begin ());
  while (it != xi.end () && jt != xj.end ()) {
    if (it->first == jt->first)
      dot += it->second * jt->second, ++it, ++jt;
    else if (it->first < jt->first)
      ++it;
    else
      ++jt;
  }
  return dot;
}

// simple QP-solver by Hildreth (1959)
std::vector <double> hildreth (const std::vector <ex_t>& ex, std::vector <std::pair <size_t, double> >& ls, const double gamma, const double c) {
  static const size_t MAX_ITER = 10000;
  static const double EPS      = 1e-8;
  static const double ZERO     = 1e-16;
  const size_t nc = ls.size (); // # constraints
  std::vector <bool>   is_computed (nc, false);
  std::vector <double> t (nc, 0.0), F (nc), kkt (nc);
  std::vector <std::vector <double> > A (nc, std::vector <double> (nc));
  //
  std::sort (ls.begin (), ls.end ()); // just for assuring the same results
  for (size_t i = 0; i < nc; ++i) {
    const ex_t&  xi  = ex[ls[i].first];
    const double dot = inner_product (xi.second, xi.second);
    A[i][i] = xi.first * xi.first * kernel (dot, gamma);
    kkt[i] = F[i] = ls[i].second;
  }
  auto max_kkt = std::max_element (kkt.begin (), kkt.end ());
  size_t max_i = max_kkt - kkt.begin ();
  for (size_t iter = 0; *max_kkt >= EPS && iter < MAX_ITER; ++iter) {
    double diff_t = A[max_i][max_i] <= ZERO ? 0 : F[max_i] / A[max_i][max_i];
    double try_t  = t[max_i] + diff_t;
    double add_t  = try_t < 0 ? -1.0 * t[max_i] : (try_t > c ? c - t[max_i] : diff_t);
    // double add_t  = try_t < 0 ? -1.0 * t[max_i] : diff_t;
    t[max_i] += add_t;
    if (! is_computed[max_i]) {
      const ex_t& yx = ex[ls[max_i].first];
      for (size_t i = 0; i < nc; i++) {
        if (i == max_i) continue;
        const ex_t&  yxi  = ex[ls[i].first];
        const double dot = inner_product (yx.second, yxi.second);
        A[max_i][i] = yx.first * yxi.first * kernel (dot, gamma);
        is_computed[max_i] = true;
      }
    }
    for (size_t i = 0; i < nc; ++i) {
      F[i] -= add_t * A[max_i][i];
      kkt[i] = t[i] > c - ZERO ? -F[i] : (t[i] > ZERO ? std::fabs (F[i]) : F[i]);
      // kkt[i] = t[i] > ZERO ? std::fabs (F[i]) : F[i];
    }
    max_kkt = std::max_element (kkt.begin (), kkt.end ());
    max_i = max_kkt - kkt.begin ();
  }
  return t;
}
#endif

// distributed passive aggressive
void train (std::vector <ex_t> &ex, f2ss_t& f2ss, std::vector <double>& alph, const long iter, const double gamma, const double c, const long m, const long n) {
  const int batch_num = ex.size () / m + (ex.size () % m ? 1 : 0);
  //
  std::map <const x_t, size_t> smap; // avoid to register identical SV
  // std::vector <size_t> smap (ex.size (), 0);
  std::vector <double> alpha; // for averaging
  std::vector <std::pair <size_t, double> > ls (std::max (n, 1L));
  size_t nround = 0;
  for (int i = 0; i < iter; ++i) {
    // std::random_shuffle (ex.begin (), ex.end ());
    for (int j = 0; j < batch_num; ++j) { // mini-batch training
      const long nex = std::min (m, static_cast <long> (ex.size ()) - j * m);
      ls.clear ();
#pragma omp parallel for num_threads (n) if (n)
      for (long k = j * m; k < j * m + nex; ++k) { // distributed training
        double m = margin (ex[k].second, f2ss, alph, gamma);
        m *= ex[k].first;
#pragma omp critical
#ifdef USE_PERCEPTRON
        if (m <= 0) ls.emplace_back (k, 0);
#else
        if (m <  1) ls.emplace_back (k, 1 - m);
#endif
      }
      ++nround;
      if (ls.empty ()) continue;
#ifdef USE_PERCEPTRON
      std::vector <double> t (ls.size (), 1.0 / ls.size ());
#else
      std::vector <double> t (hildreth (ex, ls, gamma, c));
#endif
      for (size_t k = 0; k < ls.size (); ++k) {
        if (std::fpclassify (t[k]) == FP_ZERO) continue;
        const ex_t& s = ex[ls[k].first];
        auto itb = smap.emplace (s.second, smap.size ());
        if (itb.second) {
          if (! s.second.empty ()) { // maybe empty feature vector
            if (f2ss.size () <= static_cast <size_t> (s.second.back ().first))
              f2ss.resize (s.second.back ().first + 1, f2ss_t::value_type ());
            for (const auto &f: s.second)
              f2ss[f.first].emplace_back (itb.first->second, f.second);
          }
          alph.push_back  (s.first * t[k]);
          alpha.push_back (s.first * t[k] * nround);
        } else {
          alph[itb.first->second]  += s.first * t[k];
          alpha[itb.first->second] += s.first * t[k] * nround;
        }
      }
    }
    std::fprintf (stderr, ".");
  }
  // end with averaging
  for (size_t i = 0; i < alph.size (); ++i)
    alph[i] -= alpha[i] / (nround + 1);
}

result_t test (const std::vector <ex_t>& ex, const f2ss_t& f2ss, const std::vector <double>& alph, const double gamma, const long n) {
  int pp (0), pn (0), np (0), nn (0);
#pragma omp parallel for num_threads (n) if (n) reduction (+:pp,pn,np,nn)
  for (size_t i = 0; i < ex.size (); ++i) { // distributed testing
    const double m = margin (ex[i].second, f2ss, alph, gamma);
#pragma omp critical
    if (m >= 0) if (ex[i].first > 0) ++pp; else ++np;
    else        if (ex[i].first > 0) ++pn; else ++nn;
  }
  return { pp, pn, np, nn };
}

int main (int argc, char** argv) {
  if (argc < 7) ::errx (1, "Usage %s train test gamma c iter n", argv[0]);
  const double gamma = std::strtod (argv[3], NULL); // gamma in RBF kernel
  const double c     = std::strtod (argv[4], NULL); // ignored in perceptron
  const long   iter  = std::strtol (argv[5], NULL, 10);
  const long   m     = std::strtol (argv[6], NULL, 10); // size of mini-batch
  const long   n     = std::strtol (argv[7], NULL, 10); // # processors
  if (m < n) ::errx (1, "# proc must be larger than # batch");
  //
  f2ss_t f2ss;
  std::vector <double> alph; // coefficient of SVs
  {
    // read training examples
    std::fprintf (stderr, "read (train): ");
    auto start = std::chrono::steady_clock::now ();
    std::vector <ex_t> ex = read_data (argv[1]);
    auto d = std::chrono::steady_clock::now () - start;
    std::fprintf (stderr, "%.3fs\n", std::chrono::duration <double> (d).count ());
    // distributed training
    std::fprintf (stderr, "train: ");
    start = std::chrono::steady_clock::now ();
    train (ex, f2ss, alph, iter, gamma, c, m, n);
    d = std::chrono::steady_clock::now () - start;
    std::fprintf (stderr, "%.3fs\n", std::chrono::duration <double> (d).count ());
  }
  if (std::strcmp (argv[2], "-") == 0) return 0;
  {
    // read test examples
    std::fprintf (stderr, "read (test): ");
    auto start = std::chrono::steady_clock::now ();
    std::vector <ex_t> ex = read_data (argv[2]);
    auto d = std::chrono::steady_clock::now () - start;
    std::fprintf (stderr, "%.3fs\n", std::chrono::duration <double> (d).count ());
    // distributed testing
    std::fprintf (stderr, "test: ");
    start = std::chrono::steady_clock::now ();
    result_t r = test (ex, f2ss, alph, gamma, n);
    d = std::chrono::steady_clock::now () - start;
    std::fprintf (stderr, "%.3fs\n", std::chrono::duration <double> (d).count ());
    std::fprintf (stderr, "#SV: %ld\n", alph.size ());
    std::fprintf (stderr, "acc. %2.3f%% (pp %d) (pn %d) (np %d) (nn %d)\n",
                  (r.pp + r.nn) * 100.0 / (r.pp + r.pn + r.np + r.nn),
                  r.pp, r.pn, r.np, r.nn);
  }
  return 0;
}

特徴ベクトルを正規化しているのは,RBF カーネル内積だけから計算できるようにするため.これで転置インデクスを用いた高速化手法が適用しやすくなる.正規化しなくても高速化はできるが,その場合,特徴ベクトルのノルムを別途保持する必要が出てくる.200行以内に収めたかったが,solver がこれ以上短くできなかったので諦めた.ちょっと長いかな.学習を並列化してテストを並列化しないのは片手落ちなので,テストも OpenMP で並列化.
ijcnn1 データセットで評価した結果は以下.

> g++-4.8 -Wall -O2 -g -std=c++11 -fopenmp mpak1.cc -o mpak1

// Intel(R) Xeon(R) CPU E7- 4830  @ 2.13GHz; 32CPU 512GM RAM
// 1 CPU (baseline)
> time ./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 1 0  
read (train): 0.351s
train: .....218.311s
read (test): 0.282s
test: 88.880s
#SV: 14396
acc. 99.146% (pp 8253) (pn 459) (np 324) (nn 82665)
./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 1 0  307.29s user 0.15s system 99% cpu 5:07.85 total

// 2 CPU (batch size = 256)
> time ./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 2 
read (train): 0.349s
train: .....116.360s
read (test): 0.422s
test: 44.532s
#SV: 12933
acc. 99.144% (pp 8268) (pn 444) (np 341) (nn 82648)
./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 2  306.66s user 0.04s system 189% cpu 2:41.69 total

// 4 CPU (batch size = 256)
> time ./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 4
read (train): 0.358s
train: .....53.695s
read (test): 0.599s
test: 20.578s
#SV: 12933
acc. 99.144% (pp 8268) (pn 444) (np 341) (nn 82648)
./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 4  290.27s user 0.04s system 385% cpu 1:15.26 total

// 8 CPU (batch size = 256)
> time ./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 8
read (train): 0.349s
train: .....27.922s
read (test): 0.586s
test: 10.714s
#SV: 12933
acc. 99.144% (pp 8268) (pn 444) (np 341) (nn 82648)
./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 8  307.43s user 0.05s system 776% cpu 39.602 total

// 16 CPU (batch size = 256)
> time ./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 16
read (train): 0.349s
train: .....15.029s
read (test): 0.618s
test: 6.251s
#SV: 12933
acc. 99.144% (pp 8268) (pn 444) (np 341) (nn 82648)
./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 16  325.13s user 0.07s system 1459% cpu 22.280 total

// 32 CPU (batch size = 256)
> time ./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 32
read (train): 0.352s
train: .....12.206s
read (test): 0.284s
test: 4.773s
#SV: 12933
acc. 99.144% (pp 8268) (pn 444) (np 341) (nn 82648)
./mpak1 ijcnn1 ijcnn1.t 20.0 0.5 5 256 32  520.24s user 0.13s system 2948% cpu 17.650 total

というわけで綺麗にスレッド分散の台数効果*1が出ている.また,バッチサイズを大きめにとることでオンライン学習にバッチ学習のフレーバーが入ってサポートベクタの数が減っている点は注目に値する.PA-I による(カーネルを用いた)非線形学習ではサポートベクタ数が増えがちなので,これは嬉しい.C++11 で線形分類器の分散オンライン学習器を実装してみた - ny23の日記で実装を紹介したIterative Parameter Mixingに比べると,線形学習を高速化するのにはあまり向かないが*2非線形学習や構造学習のような訓練例の分類コストの大きい学習はより効率的に高速化できそうだ.
しかし,この並列化手法もそうだけれど,自分の専門分野で「高速化」を謳う論文は,計算量が非線形であるなど,極端に遅いモデル*3を速くする,というものであることが多い.実用的には,最速のモデル*4を速くしないと解ける問題の規模は変わらないので,あまりインパクトがないと感じるのだけど(どうなのだろう).
決定的なモデルでほとんどの問題が解けるタスクで,必要以上に解空間を広げて「遅く」したモデルなんて,速くできて当たり前ではないか.計算の枝刈りができまくるのが目に見えている.なんだか自分で生み出した問題を自分で解いている感じがしてしまうのだが(しょうがないか)*5.低速・高精度の手法を提案→高速化で論文二本書くのは「身から出た錆」研究みたいでなんだかやるせない.最初から高速・高精度の手法(解空間を広げても遅くならないよう工夫)を提案したいな.
[追記]この研究の場合,構造学習を並列化するなら,古典的な Viterbi ベースの構造化パーセプトロン(や PA-I)でなくて,近年提案されているより高速なオンライン構造学習手法を高速化して欲しい.また,この記事で紹介したカーネルを用いた非線形分類器にしても,素性空間を陽に写像して線形分類器に変換・あるいは近似することで学習・分類を高速化手法があるので,本当はそちらで実験する方が望ましい*6(分類コストが下がってパラメタ更新コストが上がるので,台数効果に影響が出ると思われる).

*1:コア数と同じ数のスレッドを生成した場合に台数効果が出ていないのは,キャッシュなどでリソースの競合が起きているためと考えられる.

*2:線形学習ではパラメタ更新のコストが分類コストとほとんど同じなため,分類のみ並列化してもあまり意味が無い.学習が進むにつれパラメタの更新頻度は減ってくるので,全く意味が無いということはないと思うが.

*3:動的計画法による大域最適化とか.分類器で言えば,非線形分類や構造分類など.

*4:点予測や,構文解析の Shift-Reduce モデルなど.分類器で言えば,線形分類.

*5:とはいえ自分も人のことは言えない.ほとんど文脈自由文法で記述できる自然言語に対して,一部の事象を説明するために解析の計算量が大きい形式文法を導入して解析を高速化してみたり.

*6:RBF カーネルの場合は近似しかできないので,厳密解法を並列化するのにはかろうじてまだ価値があるが.

自分のデモの無力さを痛感した@オープンラボ二日目

オープンラボ二日目以降,オープンラボが終わっても継続的に忙しくてブログを更新する暇が取れなかった.二日目は印象的なお客さんがたくさんいらしてくれたが,その中でも先日のブログを見て来てくれた古くからの友人と(あまり詳しくは書かないが)デモを体験して頂くことが困難なお客さんが印象に残った.デモを作るときはいくつか典型的なシナリオ(使い方)を用意するのだけど,オープンラボのように年齢も職業もさまざまなお客さんが来る場合には,どうしてこのようなデモを作ったのか,またこのデモでどのようなことが分かる(できる)のかを,そのお客さんに合わせたシナリオで説明できないといけない,という当たり前の事実を再確認にすることとなった.友人氏には約束通り,デモと珈琲でおもてなし.楽しんで頂けただろうか.しかし,珈琲通でもある彼が来るのであれば,自宅からネルを持ってきてネルドリップで入れてあげればよかったかなと思ったり.
打ち上げでは,彼が仕事の負荷が限度を超えると酒量が増える - ny23の日記の事件を知って不憫に思って地ビールを3本*1も差し入れしてくれたのでみんなでありがたく頂いた.休日に朝から一日がっつり働いたあとでもあったので,本当に美味しかった.この場を借りて感謝.ちなみに,先日の出来事について誤解がないように書いておくと,職場の冷蔵庫にビールを置いていたのは,職場に来る途中で偶然に限定ビールを見かけたので思わず買ったからであって,職場で飲もうと思って買ったわけではありませぬ*2
その後,ボスとサシで自分の人生の話や学生の指導の話(学生に博士進学を進める上での判断基準や,卒業した学生が学会出張する場合に大学として何ができるか)などを話した.短い時間にかなり内容の濃い話をした記憶があるのだけど,だいぶお酒が入った段階で話し始めたのであまり覚えていない.ただ,ざっくばらんに言いたいことを何でも言ってしまった記憶だけが残っている.この辺りは酒の効用というか,アルコールが入らないと話せないことがあるなという感じで,色々と話してだいぶ胸の支えがとれた.
最後に日本の主要メーカーの某生ビール(発泡酒ではない)を久しぶりに飲んでみたら,これが生臭くて最高に不味かった.誰かと飲む場合は,話すことを楽しむので料理も酒もそんなにこだわって食べたり飲んだりはしないのだけど,それでも限度はあるのだなあ,と思った.その前に,地ビールやらベルギービールやらさんざ飲んでいたというのもあったのかもしれないけど,ちょっとあんまりなお味だった.
うーむ,なんだかお酒のことしか書いていないな.後日,前述の友人氏とビアバーで飲むこととなったのでそれまで禁酒(というか禁ビール)しよう.今月は査読祭りになる予定・・・これ以上増えないことを祈る.

*1:COEDO の紅赤,ヤッホーブルーイングのよなよなエールイとンドの青鬼.色々と分かっていらっしゃる.

*2:それ以前に自分の立場的にこのような行動をそもそもとって良いのか(書いて良いのか)という道義的な問題は残るわけだけど.このブログは,匿名ではあるが匿名でしか書けないことは書かないというポリシーで書いているので,今後はもう少し自粛しようと思っている.

MacBook Air (Mid 2011) の MagSafe 電源アダプタが壊れそう

Apple MagSafe 電源アダプタを修理しようとしてみた - ny23の日記MacBook (Late 2008) の MagSafe 電源アダプタが壊れたという話を書いたが,つい一昨日,ふと気がついたら MacBook Air (Mid 2011) の MagSafe 電源アダプタのコードで一番負荷がかかるところの被覆が避けて,中の銀線が派手に折れ曲がって飛び出していた.どうも,ずっと押されて負荷がかかっていたらしい.電源アダプタが断線するのはこれで3回目ぐらい.
(まだ問題なく使えるけど)安心して充電できないので,生協で新しい電げアダプタを調達した.そろそろ本体も換え時ということなのかな.
以前,Good Design 賞で見かけた超小型 AC アダプタ mCube mini(の65W版; 67.5g),サードパーティ製の MagSafe アダプタと組み合わせれば使えそうだけど,どうなのだろう?ケーブル込でも純正 (198g) の半分ぐらいの重量で済みそうだけど.