【初参加】BRM720 鬼怒川 200km に参加した

この記事の概要

今回はブルベというものに初参加したのできっかけや前日準備や当日の流れ,帰宅するまでを書きます.

ブルベとは

ブルベ(Brevet)はフランス語で「認定」を意味します. 制限時間内に特定の距離を特定のルートで走り切ることでブルベを運営している団体から認定を受ける耐久サイクリングイベントです. 日本でブルベを運営している団体は「オダックス・ジャパン」という非営利団体です. オダックス・ジャパンがブルベについて書いているので詳しくはそちらを御覧ください. www.audax-japan.org

ブルベに参加したきっかけ

元々,ロングライドに興味があった

大学生までは北海道にいていろいろなところに走りに行ってた.

  • 札幌 <-> 室蘭
  • 札幌 <-> 旭川
  • 室蘭 <-> 余市
  • 北海道一周
  • 日本縦断
  • etc

社会人になり研修で話した人がブルベ走っている人だった

大学生の頃からブルベには興味があったが,参加したことはなかった.今年,社会人になり実際に400kmのブルベを何回か走り抜いているという他社の人にと話す機会があった.そこでブルベへ興味が再燃して,4月に7月20日のブルベに申し込んだ.

参加したブルベ

自分が参加したブルベの概要

  • 距離 200km
  • 獲得標高 2,060m
  • 制限時間 13時間半
  • スタート・ゴール 東京 -> 栃木県 鬼怒川温泉
  • 運営 オダックスランドヌール日本橋

sites.google.com

自分は温泉が大好きで一人でチャリを走らせて入りに行くくらい好きだったのでちょうどゴールが温泉であるBRM720にした. いろは坂を通る獲得標高が2,324mという普段なら避けるレベルの登りがあったが,「なんとかなるっしょ」くらいの気持ちで申し込んだ.

参加前日

当日の3日前まで全く準備していなかった. 当日2日前に10日前くらいに届いたブルベに関する郵送物などの確認を行った.注意事項やキューシートを印刷するように書かれていた. そこには宿泊施設のご案内と書かれていたが,よくよく考えたら自分はブルベだけ申し込んでおり,ゴールしたあとのことを考えるのを忘れていた.「これはやばい」と思い,楽天トラベルで手頃な旅館をポチった.

他にも正確なルートで走らなきゃいけないのにキューシートを見るための装備が全然整ってなかった.ガーミンなどの機器も持っていなければ,スマホを自転車に固定できるような台も持っていなかった.即決でガーミンを買うほどのお金を持ち合わせていなかったので,2,000円くらいのスマホ台をアマゾンでポチった.

https://www.amazon.co.jp/gp/product/B07P8TDYQC/

キューシートはルートラボにGPXデータがあったので,これをStravaにインポートして当日はStravaをスマホから起動してキューシート代わりにすることにした.GPXデータをStravaにインポートする方法はぐぐったら割とすぐでてきた.

monoooki.net

ブルベは反射ベストの着用が義務付けられているのでカバンを背負って走ることができない. 必要最低限の装備をサドルバッグに入れて,着替えはカバンに詰め込んで郵送することにした.

問題はミニフロアポンプである.普段は背負っているカバンに入れて走るが,今回はそのカバンがないので,自転車にくくりつけようと考え自転車にミニフロアポンプを固定できる器具みたいなものを特に説明を見ずにアマゾンでポチった.

https://www.amazon.co.jp/gp/product/B004BUC60C/

スマホ台とミニフロアポンプの固定具は出発前日に届いた.危なかった... 家に帰ってからポンプの固定具をポンプにつけようとするが径が合わない.固定具についているゴム製の固定フックを思い切り伸ばすが,ポンプの径が大きすぎて回らない.力いっぱいに引っ張ると,買ったばかりの固定具のゴムを引きちぎってしまい使い物にならなくなってしまった.

前にパンクしたチューブの亡骸が家にあったので,これを切って,ポンプと自転車のトップチューブに結ぶと結構がっちり固定されたのでこれで行くことにした,チューブ紐は北海道一周や日本縦断の荷造りにも多いに役立ったのでこいつのことは割と信頼している.

当日

〜スタート地点

7時半にスタートでスタート地点までは15kmくらいだった. 5時半に起床し,6時半に家を出た.北海道の感覚ならこれで間に合うのだが,東京は信号が多すぎて全く進まない. 6kmを残しスタートまで15分を切っていた.DNSが頭をよぎった. 結局,10分遅れでスタート地点に到着するとスタッフ以外誰もいなかった. スタッフの方々は優しく,ブルベカードにサインしてスタートを見送ってくれた. まずはDNSにならなくてよかった.

〜通過チェック1 道の駅さかい

Stravaに入れたGPXもいい感じに見れてしばらく走っていると,ブルベの参加者がいたので挨拶した. 途中まで一緒に走っていたが,自分が油断して道を間違え速攻ではぐれた.

通過チェック1はスタートから55kmの地点にあるが,俺がクソ雑魚すぎて休憩なしで行けず, 5km手前のコンビニで休んでパン買って食べた.

休んだあとに無事通過チェック1である茨城県の「道の駅さかい」に到着した. 到着しても誰もいなかった.参考時間最遅の10分前くらいというギリギリだからか. 完走後の証拠になる看板の写真を一枚撮って,次のチェックポイントに向かう. f:id:muscle_keisuke:20190728230050j:plain

〜PC1 ファミリーマート藤岡町

ひたすら利根川沿いを走る.土曜日なのにサイクリストもランナーも全然いない. 途中から渡瀬川に分岐し,その川からも離れていき,草むらの中を走る感じになっていった. f:id:muscle_keisuke:20190728230053j:plain

膝が痛みだしてきた.最近,自転車の乗り方が変わってしまったのか,50km以上走ると膝が痛みだすようになってきた. 辛い中走っていると,道がなくなり砂利道になった.しかし,キューシートにはこのまままっすぐ進むように書いてある. 本当にこっちで合ってるのか疑心暗鬼になりながら1km程ある砂利道を恐る恐る進んだ.

無事,県道にぶち当たってPC1であるファミリーマートに到着した.いつの間にか栃木県に入っていた. 最遅参考時間の10分前くらいだったがお腹が減って死にそうだったのでパンとそばを買って食べることにした. 店員さんがレシートをくれなかったのを店を出てから気づいた. 完走後の証明に必要なので慌てて戻り,レシートをもらった.出発してからじゃなくてよかった.

急いでそばとパンを食べて出発した.このとき既に参考時間から15分くらい遅れていた.

〜PC2 ローソン日光東照宮前店?

お腹が苦しいのと蒸し暑さで死にそうになりながら進む. 意志が弱すぎて15km進んだ頃にまたコンビニに寄って今度はお水とアイスを買って摂取した. 体感温度が下がったところでまた出発したが15km進んだところでまた水とアイスを買いにコンビニへ寄った.

膝もだんだん痛くなってきた. 日光市入る手前くらいから登り基調になってきた.膝に追い打ちがかかる. ふくらはぎやももも攣り始めていた.コンビニに寄ってふくらはぎやももを伸ばすストレッチをしたりした.

ここらで雨も降ってきて完全に心が折れかけていた.

ずっと国道を走っていたが,途中から国道を外れ何もない道になった.人も車も通らない一本道をひたすら一人で走っていた.登りがどんどんきつくなってきたのでダンシングしようと足に力を入れた瞬間,ふくらはぎに鋭い痛みが走った. 右足のふくらはぎが完全に攣ってしまった.車も人もいない田舎道で一人で悶え苦しみ虚しくなった.

時速5kmくらいで坂を登り,なんとかPC2まで残り4kmくらいまで来たところで参考時間を過ぎた. ここでDNFを決意した.

PC2のあるコンビニとは違う道を行き,街を出る手前のコンビニでファミチキを買って食べながらDNFの連絡をした.

〜ゴール地点

今回のブルベはいろは坂を登って下るという回り道をすることで200kmとしている為,この周り道をしなければ残り14kmくらいでゴールである鬼怒川温泉に到着する.

私はDNFをしたが,鬼怒川温泉に宿をとって荷物も郵送しているので残り14kmを頑張って走ることにした.

土砂降りの中登り基調の坂を登る. すると,反射ベストを着た他のブルベ参加者にどんどん遭遇するようになってきた. 自分がいろは坂の分ショートカットしたのでいろは坂を既に降りてきていた強い人達に追いついてしまっていた. 「お疲れ様です」と声をかけられたが,DNFしているので複雑な気持ちになりながら挨拶を返した. さすが,10時間位でゴール手前まで来てる人達なだけあって,あっという間に抜かれて見えなくなった.

自分は自分のペースで坂を登り,どうにかして鬼怒川温泉駅に到着した. あたりは既に真っ暗で人も全然いなかった. f:id:muscle_keisuke:20190728230125j:plainf:id:muscle_keisuke:20190728230131j:plain

写真を撮っていると,ブルベ参加者の人がやってきた. すでにゴールしてこれから電車に乗って東京に帰る人だった.200km走って日帰りは自分には到底できないなと思いながら見ていた.

駅周辺を探索していると足湯があった.せっかく温泉に来たので足の疲れを癒やす為に入ることにした. f:id:muscle_keisuke:20190728230144j:plain

入っていると,輪行袋を持った他の参加者が入ってきたので話しかけた. 俺「いやー,疲れましたね」

相手「ほんと,特にいろは坂はやばかったですね」

俺「...」 いろは坂は登っていないのだ

DNFしたことを告げた. その人は10時間くらいでクリアしていたので本当に速い人だと思うが蔑まれることもなく励まされた. ブルベはレースではないし,各々が自分のペースで走り切ることがメインの競技なので,誰が誰を下に見るとかそういう雰囲気は結局,最初から最後までなかった.

これから電車で帰るらしいのでそれを見送って自分も足湯を出て,旅館に向かった.

鬼怒川温泉を堪能する

今回はホテル大滝という旅館に泊まることにした. とりあえず,自転車を入り口近くに停めてフロントでチェックインした. 自転車は旅館の入り口の中に置くことを許可してもらった.

結果的に言えば,ホテル自体は大満足だった.

  • 1部屋10畳一人6300円
  • 繁忙期から外れ人も少なく快適
  • ボードゲームや本,DVDプレイヤーも貸し出していた
  • 温泉も建て替えたばかりできれいでひのきのいい香り
    f:id:muscle_keisuke:20190728230137j:plain
    泊まった部屋,これで6,300円は安いと思った

とりあえず,汗で汚い体を部屋のシャワーでキレイにしてフロントの人に聞いたおすすめの定食屋さんに行った. 今回は「佃政」というお店に行った.ハンバーグが美味しいらしい. ハンバーグを頼んだあとにメニューを見てると,「湯波刺し」という珍しいメニューがあった. 調べると栃木県の日光市は「湯波」が有名らしい.「湯葉」といえば,京都だが日光も有名で区別して「湯波」と表記するらしい.

icotto.jp

それなら,湯波を頼めばよかったと少し後悔した. その後,何気なく日本酒もメニューを見てみると,自分が大好物の「鳳凰美田」があった. 「鳳凰美田」は栃木県の小林酒造で作っている日本酒であることにそこで初めて気づいた. 大後悔した.

「湯波刺し」をつまみに「鳳凰美田」をやりたかった...

しかし,お金もかかるし,今日は我慢してハンバーグだけ食べることにした. ちなみにハンバーグもふわふわでジューシーでとても美味しかったです. f:id:muscle_keisuke:20190728230154j:plain

ご飯を食べたあとはコンビニで500ml缶のアサヒィ↓スゥパァ↑ドゥルァァァァイ↓とつまみを買って帰った. この後はメインの風呂だ.

風呂は離れにあって一度外に出る必要がある. 足場が悪くてスリッパで移動する必要があるのでそこは少しマイナスポイントだった.

しかし,去年建て替えたばかりだけのこともあって脱衣所も風呂場も檜のとてもいい香りがした. 風呂の種類は少ないが,柵がない全面に開けた露天風呂もあって最高だった. 内風呂も吹き抜けからおしゃれな照明が吊るしてあってとてもいい雰囲気だった.

大満足で風呂から上がり部屋でビール飲んで就寝した. ロングライドの後のビールが一番うまい.

帰宅日

鬼怒川温泉を堪能する(2日目)

今日は昨日食べられなかった湯波を食べてから帰ると決めていた.

しかし,湯波を置いてある店が開くのが10時なのでまだ時間が2時間程余っていた. とりあえず,朝風呂に入った.昨日真っ暗で見えなかった露天からの景色と貸切状態の風呂を楽しんだ.

10時にチェックアウトして湯波が置いてある店前の「杉ん子」という定食屋に入った. ここには毎日15色限定の「湯波丼」というものがあるらしい. ご飯に湯波をかけてその上にあんかけやきのこを乗せた丼らしい. 限定という言葉に弱く,それを頼んだ. f:id:muscle_keisuke:20190729004547j:plain

初めて湯波を食べたが,あれは食感を味わうものなのだろうか.湯波自体に味はあまりなかったが食感がとろとろで大豆の風味をほんのりと感じた.丼自体は美味しかった.

完食後,昨日の湯波刺しが忘れられず,結局追加で湯波刺しと目に付いた地ビールを頼んで散財してしまった. f:id:muscle_keisuke:20190729005007j:plain f:id:muscle_keisuke:20190728230202j:plain f:id:muscle_keisuke:20190729005012j:plain

どれも美味しかった.店入ってから出るまで誰も来なかったので店のおばちゃんとずっと話しながらご飯食べてた.

店を出て隣のお土産屋で会社へのお土産を買って,お酒が入ってしまったので駅まで自転車を押していった. 駅前に猿と人がいて何やら芸の準備をしていたが,自転車を分解しなければならなかったので猿の芸に湧く歓声を背にして輪行の準備をしていた. 乗り換えを極力したくなかったので特急券を買ってそれで浅草まで乗って乗り換えて無事家まで帰った.

まとめ

  • 梅雨続きで1ヶ月以上乗ってなかったのもあって簡単に足を攣ってしまってそのままDNF
  • ブルベは周りの人優しい
  • 遅刻しても参加を許可してくれたスタッフ
  • DNFしても励ましてくれる参加者
  • 次回,再挑戦したい
    • 200kmに慣れる
    • 乗り方を改善する
  • 鬼怒川温泉よかった
    • 旅館は安くていい部屋と風呂
    • 湯波も美味しかった
  • 次は電車で行って酒造巡りたい

自転車で北海道一周してきたのでGPSログをLaTeXで表示する

この記事はTeX & LaTeX Advent Calendar 2018 18日目の記事です. LuaTeX関係ありません.遅くなってしまって申し訳ありません.

はじめに

私はサイクリングが趣味で,休みの日はロングライドに出かけることもあります. そして,タイトルにある通り学生最後の夏休みに17日かけて室蘭市から北海道を反時計回りに出発し函館市までツーリングをしてきました.

f:id:muscle_keisuke:20181218162317j:plainf:id:muscle_keisuke:20181218162322j:plain
f:id:muscle_keisuke:20181218162328j:plainf:id:muscle_keisuke:20181218162342j:plain

私は自転車でロングライドする時には必ずGPSログを取ります.北海道一周のときにもこのログは取り続けていました. 自分が保存していたGPSログはGPXというフォーマットです. GPXファイルには位置,速度,標高などの情報がXMLスキーマベースで保存されています. 全てテキストデータになっています.

そこで思いつきました.

LaTeXGPSログを読み出せそう

ということで,GPXファイルをLaTeX上で読み込んで,そのルートをTikZで描画することにしました.

GPXファイルの中身をLaTeXで読む

GPXファイルについて

GPXファイルは以下のようなXMLスキーマベースで記録されています*1

<?xml version="1.0" encoding="UTF-8"?>
<gpx creator="StravaGPX" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
 <metadata>
  <time>2017-09-04T00:57:35Z</time>
 </metadata>
 <trk>
  <name>Morning Ride</name>
  <trkseg>
   <trkpt lat="42.3847010" lon="141.0319980">
    <ele>65.4</ele>
    <time>2017-09-04T00:57:35Z</time>
   </trkpt>
   <trkpt lat="42.3846550" lon="141.0323940">
    <ele>64.9</ele>
    <time>2017-09-04T00:57:39Z</time>
   </trkpt>
   ...
  </trkseg>
 </trk>
</gpx>

<trk>というタグがトラックの始まりを表します.トラックには時間や緯度経度,標高などが記録されています. それぞれの数値はタグ内に記録されており,対応するタグは以下の通りです.

  • <trkpt>(緯度経度)
  • <time>(時間)
  • <ele>(標高)

今回はPDF上にルートを表示したいので緯度経度情報のみ取り出すことにします.

LaTeXでファイルの中身を読み出す

ファイルの読み込み

LaTeXにはファイルを読み出すコマンドがあります. 以下のソースはファイルを読み込み1文ずつ\filelineに格納していくプログラムです.

\newread\file
 \openin\file=filename
 \loop\unless\ifeof\file
  \read\file to\fileline
 \repeat
\closein\file

これを使ってGPXファイルから緯度経度情報の一覧を取り出します.

ある文字列を含む文のみ取り出す

LaTeXでファイルから1文ずつ取り出すことはできました. 次は必要な文だけ取り出すことを考えます. 緯度経度情報はGPXファイルの<trkpt>タグ内に記録されています. 次のようなフォーマットです.

<trkpt lat="緯度" lon="経度">...</trkpt>

ここから緯度,経度の数値のみ取り出します. まずは<trkpt ... を含む文章を取り出します. xstringパッケージは文字列処理に関するマクロが定義されています. その一つの\IfSubStrコマンドは文中に特定の文字列が入っているかどうか判定することができます. これを使って条件分岐を行います.

\IfSubStr{\filename}{<trkpt}{
 ...
}{}

第一引数に文,第二引数に含まれるか判定する文字列を渡します. 第一引数に今回は取り出した文が入った\filenameを指定し,<trkptを第二引数に指定します. これで<trkptから始まる文のみに分岐して処理を進めることができます.

次に取り出した文から緯度経度の数値のみ取り出します. 同じくxstringパッケージの\StrBetweenコマンドを使えば 特定の文字列に挟まれた文字列を取り出すことができます.

\StrBetween{}{取り出したい文字列の前の文字列}{取り出したい文字列の後の文字列}[取り出した後に格納する変数]

まずは緯度を取り出します.以下のフォーマットなので,

<trkpt lat="緯度" lon="経度">...</trkpt>
\StrBetween{\fileline}{lat="}{" lon}[\gpxlat]%

で取り出し,\gpxlatに格納します. 経度も同じように

\StrBetween{\fileline}{lon="}{">}[\gpxlon]%

で取り出し,\gpxlonに格納します. これで,

<trkpt lat="42.3847010" lon="141.0319980">

から 42.3847010141.0319980のみ取り出すことができました.

地図を表示する

地図表現の手法について

地図を表示しなければルートを描画しても位置がわかりにくいのでルートの下に地図を表示します. 最初のアイデアは地図をベクタ画像で表示し,その上に緯度経度から平面座標に変換した点をプロットして点間を線で結び, ルートを描画する方法でした. しかし,後述する複雑な数式によって,単純な平面座標への変換を断念したため, 今回は正方形タイルによる地図表現*2という手法を使い地図の表示をします. この手法はGoogleにより考え出された手法であり,現在,様々な地図アプリに採用されています. 平面座標への変換がしやすいという点で今回採用しました.

正方形タイルによる地図表現

正方形タイルによる地図表現は地図を縦横方向に敷き詰めた正方形タイルで表現する手法です. ズームレベルというものを定めズームレベルが高くなるにつれ, 敷き詰める正方形タイルを多くしてより詳しく地図を表現します.正方形タイルのピクセル数は256 x 256で統一され, ズームレベル0を初期状態とし,タイル数1で世界を表現します.

http://www.trail-note.net/images/zoom0.png

© OpenStreetMap contributors

ズームレベル1で縦横それぞれ2倍のタイル数になり2 x 2になります.

f:id:muscle_keisuke:20181219000859p:plainf:id:muscle_keisuke:20181219001114p:plain
f:id:muscle_keisuke:20181219001130p:plainf:id:muscle_keisuke:20181219001139p:plain
© OpenStreetMap contributors

つまり,ズームレベルnでタイル数は縦横それぞれ 2^n x  2^nとなります.

タイルにはそれぞれタイル座標というものがあり,例えばズームレベル1であれば, 左上のタイルのタイル座標は(0,0)で,右下のタイルのタイル座標は(1,1)となります. タイル座標とは別にピクセル座標というものもあります.これは全タイルを含めた総ピクセル数の内, どこの座標を指し示すかを表す座標です.ズームレベル1の場合256 x 256 のタイルが2 x 2敷き詰められているので, 総ピクセル数は512 x 512となります.よって,タイル座標(0,1)の中心点のピクセル座標は(384,128)となります.

地図を入手する

この正方形タイルに対応した地図を無償で公開している国土地理院から画像ファイルを持ってきます.

地理院地図|地理院タイル一覧

様々な地図をズームレベル毎にダウンロードすることができます. 今回は標準地図を使わせてもらいました.また,ズームレベル毎にタイル画像を用意すると大量になってしまうので, 今回はズームレベル11で固定しました. ズームレベル11の北海道本土のタイルをあらかじめすべて用意します. 今回用意したタイルのタイル座標は(1818,731)から(1854,764)です. 37 x 33 = 1221枚のタイルをLaTeXで貼り付けることになります. 貼り付けは\foreachを使って行います. \spreadtileというマクロを定義し,第一引数,第二引数に左上のタイル座標を渡し,第三引数,第四引数に縦横方向のタイル数を渡します. 今回は(1818,731)から37 x 33のタイルを敷き詰めるので \spreadtile{1818}{731}{37}{33}のように引数を渡します.

\newcommand{\spreadtile}[4]{
  \foreach \x in {0,...,#3}{
    \foreach \y in {0,...,#4}{
      \pgfmathparse{int(#1 + \x)}
      \let\numTileX\pgfmathresult
      \pgfmathparse{int(#2 + \y)}
      \let\numTileY\pgfmathresult
      \node[outer sep=0pt, inner sep=0pt] (image) at (\x*90.31,-\y*90.31){\includegraphics[]{maps/11:\numTileX:\numTileY.png}};
    }
  }
}

タイル画像を貼る座標指定についてですが,画像サイズがピクセルになっているため, LaTeX(TikZ)内の座標系と座標に変換する必要があります. tikzpicture環境は座標の刻み幅をxyそれぞれ1mmに設定しています.

\begin{tikzpicture}[x=1mm, y=1mm]
...
\end{tikzpicture}

なので256 x 256ピクセルの画像をmm単位に変換します. LaTeXのデフォルトのdpi(dot per inch)は72なので,1インチあたり72ピクセルのドットが入ります. 1ピクセルあたり1/72インチなので,256/72インチ(=3.555...インチ\approx90.31mm)になります. つまり,PDF内に原寸大で貼り付けられたタイル画像は90.31mm x 90.31mmになります. 次に座標系の変換です.といってもy軸方向の正負の向きが逆なだけです. よって,(x,y)番目の画像を貼り付ける座標は(90.31x, -90.31y)となります.

これで,地図タイルを隙間なく敷き詰めることができました.

緯度経度を平面座標に変換する

緯度経度 -> ピクセル座標 -> mm座標に変換する方法

PDFに描画するために緯度経度を平面座標に変換する必要があります. 単純に緯度経度を平面座標に変換する式は以下のリンクにありました. https://vldb.gsi.go.jp/sokuchi/surveycalc/surveycalc/algorithm/bl2xy/bl2xy.htm しかし,数式が複雑過ぎてLaTeXで計算するのが困難です. 数式を書くのは簡単なんですけどね

上記の理由により,地図表現を正方形タイルによる方法に変更しました. 正方形タイルによる地図表現の場合,以下の情報が分かれば,平面座標を求めることができます.

  • \lambda : 緯度
  • \varphi : 緯度
  • z : ズームレベル

緯度経度からピクセル座標に変換する数式は以下の通りになります.


x = 2^{z+7}\left(\frac{\lambda}{180} + 1\right)


y = \frac{2^{z+7}}{\pi}\left(-\mathrm{tanh}^{-1}\left(\sin\left(\frac{\pi}{180}\varphi\right)\right) + \mathrm{tanh}^{-1}\left(\sin\left(\frac{\pi}{180}L\right)\right)\right)


L = 85.05112878

xを求める式を式1,yを求める式を式2とします.

引用:http://www.trail-note.net/tech/coordinate/

上記の式によって求まる座標は前節で説明したピクセル座標ですが,全世界における絶対座標になります. 今回は北海道のみしか地図に表示していないため,これをタイル内における座標に変換します. まず,求めた絶対座標を(x,y)としたときのタイル座標を求めます. タイルは256 x 256ピクセルなので x,yをそれぞれ256で割ると,割った商がタイル座標になります. 更に割った余りが求めたタイル座標内におけるピクセル座標となります. これを前節で説明した方法でmmに変換すると,PDFに表示された地図上に適切に点をプロットすることができます.

LaTeXによる実装

変換方法が確立したので,次はそれをLaTeXで実装します. しかし,LaTeXは複雑な計算をするとコンパイルに時間がかかってしまうので, 定数など計算可能な部分は先に計算します. 式1はこれ以上計算のしようがないのでこのままLaTeXで計算します. #2が経度,#3がズームレベルです.

\FPeval{\abpixcelX}{2^(#3+7) * (#2/180 + 1)}

問題は式2です.まず,式内の逆双曲線関数LaTeXに求める関数がないので,これを


\mathrm{tanh}^{-1}z = \frac{1}{2}\ln\left(\frac{1 + z}{1 - z}\right)

で変形します. すると,式2は


y = \frac{2^{z+6}}{\pi}\ln\frac{\left(1 - \sin\left(\frac{\pi}{180}\varphi\right)\right)\left(1 + \sin\left(\frac{\pi}{180}L\right)\right)}{\left(1 + \sin\left(\frac{\pi}{180}\varphi\right)\right)\left(1 - \sin\left(\frac{\pi}{180}L\right)\right)}

のように変形できる. L = 85.05112878より,


y = \frac{2^{z+6}}{\pi}\ln\frac{1.99627207622\left(1 - \sin\left(\frac{\pi}{180}\varphi\right)\right)}{0.00372792378\left(1 + \sin\left(\frac{\pi}{180}\varphi\right)\right)}


= \frac{2^{z+6}}{\pi}\ln\frac{\left(1 - \sin\left(\frac{\pi}{180}\varphi\right)\right)}{\left(1 + \sin\left(\frac{\pi}{180}\varphi\right)\right)} + \frac{2^{z+6}}{\pi}\ln\frac{1.99627207622}{0.00372792378}


= 2^{z+6}\left(2 + \frac{1}{\pi}\ln\frac{\left(1 - \sin\left(\frac{\pi}{180}\varphi\right)\right)}{\left(1 + \sin\left(\frac{\pi}{180}\varphi\right)\right)} \right)


= 2^{z+6}\left(2 + 0.31830988618\ln\frac{\left(1 - \sin\left(\frac{\pi}{180}\varphi\right)\right)}{\left(1 + \sin\left(\frac{\pi}{180}\varphi\right)\right)} \right)

となります.これをLaTeXで計算します.

  \FPeval{\abpixcelY}{2^(#3+6)/pi * (ln(1.99627207622*(1 - sin(pi*#1/180))/(0.00372792378*(1 + sin(pi*#1/180)))))}

これで絶対ピクセル座標が求まる.次にタイル座標を求め,タイル内ピクセル座標を求めます.

\FPeval{\tileX}{trunc(\abpixcelX / 256, 0)}
\FPeval{\tileY}{trunc(\abpixcelY / 256, 0)}

余りを求める式がFPパッケージにないため, 割った値の小数点以下を切り捨てた後,商を掛け,元の値との差を計算して余りを求めます. また,ピクセル座標は左上が原点なのに対し,LaTeX内に貼り付けるタイル画像は画像中心が原点なので, 求めたタイル内ピクセル座標に対し,座標系を変換した上で(-128, 128)の平行移動をさせます.

\FPeval{\localX}{trunc(\abpixcelX-(256*trunc(\abpixcelX/256,0)),0) - 128}
\FPeval{\localY}{128 - trunc(\abpixcelY-(256*trunc(\abpixcelY/256,0)),0)}

最後に敷き詰められた(x,y)番目の画像は(90.31x, -90.31y)なので, タイル内ピクセル画像を平行移動させ,ピクセル -> mmに変換して描画します. dpiが72で1インチ=25.4mmより,1mmあたりのピクセル数は72/25.4=2.83464566929で求まります. よって,

\newcommand{\convertmm}[2]{
  \pgfmathparse{\pixcelXonPDF/2.83468054}
  \let\mmXonPDF\pgfmathresult
  \pgfmathparse{\pixcelYonPDF/2.83468054}
  \let\mmYonPDF\pgfmathresult
}
\pgfmathparse{int(\tileX - \value{initTileX})*256 + \localX}
\let\pixcelXonPDF\pgfmathresult
\pgfmathparse{int(\localY - int((\tileY - \value{initTileY})*256})
\let\pixcelYonPDF\pgfmathresult
\convertmm{\pixcelXonPDF}{\pixcelYonPDF}

ついにプロットする点の真の座標(\mmXonPDF,\mmYonPDF)を求めることができました.

ルートの描画

LaTeXの限界

早速求めた点をプロットしようと思い,テストデータとして, 100km程度のロングライドのGPXデータを用意してコンパイルを走らせました. しかし,

! TeX capacity exceeded, ...

というエラーが起こりました. 作業領域として確保しているメモリーを使い果たしてしまったようです. 100km程度のロングライドでメモリーを使い果たしてしまうので, 2000km程度の北海道一周ツーリングをプロットするのは不可能です. なので,<trkpt...から始まる文を見つける度にカウンタをインクリメントし,100で割り切れる時のみ, プロットをすることにしました.

\newcounter{numPoint}
...
\IfSubStr{\fileline}{<trkpt}{%
 \pgfmathparse{int(mod(\value{numPoint},100))};
 \ifnum\pgfmathresult=0
  ....
  \filldraw [red, ultra thin] (\mmXonPDF, \mmYonPDF) circle (1);
  \ifnum\value{numPoint}=0\else
   \draw [red, line width = 7pt] (\mmXonPDF, \mmYonPDF) -- (previous);
  \fi
  \coordinate (previous) at (\mmXonPDF, \mmYonPDF);
 \fi
 \stepcounter{numPoint}

実際に描画した

北海道1周のGPXをすべて読み込んで,点のプロットをしてみました.

f:id:muscle_keisuke:20181219021343p:plain

100個に1つの割合で点をプロットしていますが,それでもかなりの精度でプロットできています.

f:id:muscle_keisuke:20181219133440p:plainf:id:muscle_keisuke:20181219133436p:plain
f:id:muscle_keisuke:20181219133441p:plainf:id:muscle_keisuke:20181219133445p:plain

PDFの実際のサイズは27000 x 240000になりました.

最後に

LaTeXでこれだけ作れたので,普通にWebアプリケーションとしてGPXビューワ作れそうだなと思いました. ちなみに今回の北海道1周のデータをコンパイルするのに30分近くかかります.遅すぎる.

実は北海道1周の後に日本縦断もしたので, そのデータも描画してみたかったです.今度やってみます.

LaTeXでプロデューサー名刺を作る

この記事は アイドルマスターAdvent Calendar 2018 13日目の記事になります. 遅れてしまい申し訳ありません!

これはなにか

プロデューサー歴3年目にも関わらずまだ名刺を持っていないので,LaTeXで名刺を作ってみた話です.

LaTeXとは

LaTeX は文書作成ツールで,数式を含む文書の作成が比較的簡単かつ綺麗にできるため,主に理工系の論文レポート執筆に用いられます.


あれ?

論文やレポート? どう考えても名刺作成向けじゃないよね?

はい,全く向いていないと思います. 他にやっている人もいないだろうというモチベーションで今回もやっていきたいと思います. ちなみに去年もそんな不純な動機によって作られたものがいくつかあります.

muscle-keisuke.hatenablog.com

muscle-keisuke.hatenablog.com

というわけでやっていきます.

まずはペイントツールで作りたい名刺を作る

というわけで作りました. f:id:muscle_keisuke:20181214035520p:plain f:id:muscle_keisuke:20181214035548p:plain


...? あれ?

名刺完成しちゃった!!!!

というわけで終わります.



というわけにもいかないので,完成した名刺を基にLaTeXで名刺を作っていきます. もはや何のためにやってるのかわかりません.

LaTeXで名刺を作っていく

LaTeXの設定

まずは印刷用に用紙の設定をしていきます. 適当に調べてでてきた名刺作成用の紙のサイズを参考に設定していきます.

f:id:muscle_keisuke:20181214040107p:plain 引用:https://www.biccamera.com

用紙サイズはA4,余白は横14mm, 縦11mmみたいです. この用紙の中に10枚の名刺を印刷します. geometryパッケージを使って余白を設定します.

\documentclass[dvipdfmx]{jarticle}
\usepackage{geometry}
\geometry{left=14truemm, right=14truemm, top=11truemm, bottom=11truemm}

また,LaTeXはデフォルトで字下げやページ番号などの機能が有効なので,これを無効化します.

\pagestyle{empty}
\parindent=0pt

必要なパッケージも読み込んでおきます.

\usepackage{tikz} % TikZを読み込むため
\usepackage{graphicx} % 画像を読み込むため
\usepackage{geometry} % 余白調整するため
\usepackage[deluxe]{otf} % ヒラギノ丸ゴシックを使うため
\usetikzlibrary{calc} % 座標計算のため

背景画像を表示する

それでは次に名刺作成の方法ですが,LaTeXには論文に載せる図などを描画することができるTikZというパッケージがあります. 今回はこれを使って名刺のデザインをやっていきます.名刺のサイズは91mm x 55mmなので,まずは中央の(45.5mm, 27.5mm)に 91mm x 55mmに縮小した画像を配置していきます.

\begin{tikzpicture}[x=1mm, y=1mm, outer sep=0pt, inner sep=0pt]
      \node[outer sep=0pt, inner sep=0pt] (image) at (45.5,27.5) {\includegraphics[width=91truemm, height=55truemm]{300328.png}};
      \node[outer sep=0pt, inner sep=0pt, opacity=0.8] (image) at (45.5,27.5) {\includegraphics[width=91truemm, height=55truemm]{background.png}};
\end{tikzpicture}

300328.pngはみりあちゃんの一枚絵でback_ground.pngは完成図の中で透過していた背景画像です. 背景画像の方はopacity=0.8のオプションで2割透過した状態になっています. f:id:muscle_keisuke:20181214041809p:plain

f:id:muscle_keisuke:20181214041129p:plain
300328.png

f:id:muscle_keisuke:20181214041148p:plain
background.png
引用:http://cgss-fan.xyz

しかし,これだけでは,10枚中左上の1枚にしか画像が表示されないので, TikZのforeachコマンドで10回ループさせます.

\foreach \x in {1,2,...,10}{
\begin{tikzpicture}[x=1mm, y=1mm, outer sep=0pt, inner sep=0pt]
      \node[outer sep=0pt, inner sep=0pt] (image) at (45.5,27.5) {\includegraphics[width=91truemm, height=55truemm]{300328.png}};
        \node[outer sep=0pt, inner sep=0pt, opacity=0.8] (image) at (45.5,27.5) {\includegraphics[width=91truemm, height=55truemm]{background.png}};
  \end{tikzpicture}
}

これで,10枚表示されます. f:id:muscle_keisuke:20181214042028p:plain 画像同士に自動で余白が入ってしまい,2ページにまたがって表示されてしまいます. 水平方向に余白を空ける\hspace垂直方向に余白を空ける\vspaceに負の値を入れて縦横の余白を消します.

\ifodd\x % 奇数枚目と偶数枚目の間の水平方向の余白を消す.
    \hspace{-1em}
  \fi
  \vspace{-1pt}

\xはforeachのカウンタ変数です. これで10枚の画像が1ページにまとまります. f:id:muscle_keisuke:20181214042706p:plain

背景画像の切り抜き

今の時点では,透過した背景画像が全体にかかった状態になっているのでこれを切り抜いていきます. TikZには\clipというコマンドがあり,座標指定で結んだ線で囲まれた範囲を切り抜くコマンドがあります. これを使って背景画像を完成図のように上三角と下三角に切り抜いていきます. 1枚絵を読み込んだ後に以下の行を追加します.

\begin{scope}
  \clip (15,55) -- (91,55) -- (91,15) -- (15,55);
  \node[outer sep=0pt, inner sep=0pt, opacity=0.8] (image) at (45.5,27.5) {\includegraphics[width=91truemm, height=55truemm]{background.png}};
\end{scope}
\begin{scope}
  \clip (0,40) -- (76,0) -- (0,0) -- (0,40);
  \node[outer sep=0pt, inner sep=0pt, opacity=0.8] (image) at (45.5,27.5) {\includegraphics[width=91truemm, height=55truemm]{background.png}};
\end{scope}

切り取りしてしまうと元の画像がなくなるので背景画像を上三角用,下三角用に2枚読み込んでいます. また,他の画像を切り抜いてしまわないように各clipコマンドは外のコマンドに影響を与えないscope環境の中で実行しています. これで,名刺の背景は完成です. f:id:muscle_keisuke:20181214044134p:plain

文字を入れていく

背景は完成したので必要な情報を入れていきます. 今回載せる情報は以下の通りです.

  • プロデューサー名
  • 担当名
  • TwitterID
  • デレステのゲームID

それぞれ,表示する位置座標を定義しておきます.

\coordinate (myname) at (55,50);
\coordinate (tanto) at ($(myname) + (16pt * 2.5, -16pt * 1.5)$);
\coordinate (gameID) at (23,5);
\coordinate (twitter) at ($(gameID) + (0, 16pt * 1.5)$);

担当名とTwitterIDの座標はそれぞれプロデューサー名とデレステIDの座標から 相対的に設定しています.

次に,表示ですが,下書きの段階では文字は何も考えずヒラギノ丸ゴシックを使いました. それが原因で,下書きでは和文だけでなく欧文もヒラギノ丸ゴシックになってしまっていました. ヒラギノフォントには従属欧文というものがあり,和文だけでなく欧文も用意されています. 単純な従属欧文であれば,LaTeXでも出力可能なのですが,更に従属欧文の丸ゴシックとなると出力方法が分かりませんでした. なので,欧文部分はヒラギノゴシックの従属欧文で表示することにしました.

{\fontsize{16pt}{0pt}\selectfont
        \node at (myname) {\mgfamily\userelfont\selectfont うっひょいP};
        \node at (tanto) {\mgfamily 赤城みりあ担当};
}
{\fontsize{12pt}{0pt}\selectfont
        \node at (twitter) {\mgfamily\userelfont\selectfont Twitter @keisuke495500};
        \node at (gameID) {\mgfamily\userelfont\selectfont デレステID 407845058};
}

上三角部分は文字サイズ16ptで,下三角部分は12ptにしました. また,\mgfamily和文を丸ゴシックに\userelfont\selectfontで欧文をヒラギノフォントの従属欧文で表示しています. f:id:muscle_keisuke:20181214045409p:plain 完成です.

最後に

今回は簡単なデザインだったのですぐできました. ぎりぎりまでネタが思いつかなかったので1日でやるにはちょうどいいテーマだったと思います.

次からはちゃんとペイントツールを使いたいと思います.

アイドルマスター Advent Calendar 2018 14日目はuzuforestさんの記事です。

TeXsorflow(LaTeXニューラルネットワーク)を作った

これはMuroran Institute of Technology Advent Calendar 22日目の記事です.

はじめに

ニューラルネットワーク構築用パッケージ TeXsorflow*1を作りました. つまり,

LaTeXニューラルネットワーク

実装しました.

ニューラルネットワークとは

機械学習の一分野で人間の脳を数理モデル化したものです.教師データの入力を基に誤差を算出し,誤差を少なくするように重みやバイアスを調整することで学習を行います.

f:id:muscle_keisuke:20171222093822p:plain 引用: http://zuqqhi2.com/nn-most-decent-4

作ったもの

今回はLaTeXで分類問題を解くニューラルネットワークを実装しました.教師データから学習を行い,推論フェーズで出した答えと教師データの解答をPDFに出力します.

想定する環境

データ

今回はirisのデータを使って,種(Species)の分類を行います. がく片長(Sepal Length),がく片幅(Sepal Width),花びら長(Petal Length),花びら幅(Petal Width)を入力として持ち,種(Species)を出力とします.

モデル

モデルは以下のような設定です.

  • 入力層ユニット数 4
  • 隠れ層ユニット数 3
  • 隠れ層レイヤー数 1
  • 出力層ユニット数 3

f:id:muscle_keisuke:20171222102443p:plain

実装しなければならないこと

実装

主に使うパッケージ

  • FP
  • pgfmath
  • arrayjobx
  • datatool
  • ifthen

マクロなどのグローバル化

グローバルなマクロ

今回,繰り返し処理はpgfパッケージにあるforeachコマンドを使って行います.

\foreach \x in {1,...,100}{%
  \pgfmathsetmacro{\result}{10}
}

その際に,foreach内でマクロに代入を行うと,グループ内はローカルのため,次のループに入る時,値が消えます. 上の例だと,\resultに10を代入しても,繰り返すと,次の代入まで0になります. よって,マクロを以下のようにグローバルにします.

\xdef{\result}{\result}
グローバルな配列

LaTeXにはarrayjobxという配列構造を扱えるパッケージがあります. 使い方は以下を参考にしました.

  • arrayjobx

天地有情 [LaTeX] arrayjob --- 配列データ構造を操作する

さて,このarrayjobxで生成した配列はグローバルではありません,なので,前節と同じ理由で値が消えたりします.なので,これをグローバルに対応させます.

\let\newglobalarray\newarray
\patchcmd{\newglobalarray}{\edef}{\xdef}{}{}

配列を生成する\newarrayを\newglobalarrayにコピーして,定義内の\edefを全て\xdefに置き換えて,グローバル化します.

tex.stackexchange.com

次にマクロを展開して配列に代入できるマクロを定義します. 本来,arrayjobxは以下のようにして値の代入ができます.

\newglobalarray\hogearray
\hogearray(1,1)=1

しかし,代入する対象が展開が必要なマクロなどの場合は,展開の順序を考慮する必要があります. よって,代入の対象を展開してから配列に代入するマクロを定義します.

\newcommand{\assignArray}[2]{%
  \begingroup\edef\xx{\endgroup\noexpand#1={#2}}\xx%
}%

活性化関数の実装

隠れ層の活性化関数にはReLUを使い,出力層の活性化関数にはソフトマックス関数を使います.

ReLU

ReLUとその微分は以下のような式で表します.

[tex: \displaystyle f(x) = \max(0, x)]

[tex: \displaystyle f'(x) =
\left\{
\begin{array}{l}
1 & (x > 0) \\
0 & (otherwise)
\end{array}
\right.
]

これは数式を解釈できるpgfパッケージのpgfmathsetmacroコマンドを使えば簡単に実装できます.

\newcommand{\activationFunction@texnn}[1]{%
    \pgfmathsetmacro{\x@texnn}{#1}%
    \pgfmathsetmacro{\y@texnn}{max(0, \x@texnn)}%
}%
% arg1: x
\newcommand{\activationFunctionDiff@texnn}[1]{%
    \pgfmathsetmacro{\x@texnn}{#1}%
    \pgfmathsetmacro{\y@texnn}{\x@texnn>0 ? 1 : 0}%
}%
ソフトマックス関数

出力層はソフトマックス関数を活性化関数とします. ソフトマックス関数とその微分は以下の式で表します.

[tex: \displaystyle f(x_i) = \frac{\exp(x_i)}{\sum_{j=0}^N \exp(x_j)}]

[tex: \displaystyle f'(x_i) =
\left\{
\begin{array}{l}
f(x_i)(1 - f(x_i)) & (i = j) \\
-f(x_i)f(x_j) & (i \neq j)
\end{array}
\right.
]

こちらはfpパッケージという固定小数点演算用のパッケージを使って実装しています.

% arg1: x
\newcommand{\softmax}{%
    \checkZs@iris(3,1) % 配列から値を読む(\cachedataに格納される)
    \pgfmathsetmacro{\z@one}{\cachedata} % \cachedataを別の変数に格納
    \checkZs@iris(3,2)%
    \pgfmathsetmacro{\z@two}{\cachedata}%
    \checkZs@iris(3,3)%
    \pgfmathsetmacro{\z@three}{\cachedata}%
    \FPeval{\z@denom}{(exp(\z@one)) + exp(\z@two) + exp(\z@three)} % ソフトマックスの分母部分を計算
    \FPeval{\y@texnn}{(exp(\z@one) / \z@denom)} % ソフトマックスの分子部分を計算
    \assignArray{\As@iris(3, 1)}{\y@texnn} % 計算結果を配列に格納
    \FPeval{\y@texnn}{(exp(\z@two) / \z@denom)}%
    \assignArray{\As@iris(3, 2)}{\y@texnn}%
    \FPeval{\y@texnn}{(exp(\z@three) / \z@denom)}%
    \assignArray{\As@iris(3, 3)}{\y@texnn}%
}%

CSVの読み込み

ニューラルネットワークもデータがなければ始まりません.CSVを読み込んで配列に格納します.

外部ファイルからの読み込み

外部からCSVを読み込むにはdatatoolパッケージを使います.また,それを配列に格納するために配列データ構造を使えるarrayjobxパッケージを使います. 使い方は以下を参考にしました.

  • datatool

天地有情 [LaTeX] datatool --- CSVデータからグラフやテーブルを作成

\DTLloaddb{irisDB}{iris_random.csv} % csvを読み込む
\newcounter{step@dtl} %データ数をカウント
\setcounter{step@dtl}{0}%

\dataheight=\value{inputNum} % 列数を指定し,一次元配列を二次元として扱う
\DTLforeach{irisDB}{\slIris=sepalLength,\swIris=sepalWidth,\plIris=petalLength,\pwIris=petalWidth,\correctIris=species}{%
        \stepcounter{step@dtl}%
        % 各列を配列に代入
        \assignArray{\irisSVArray(\value{step@dtl}, 1)}{\slIris}%
        \assignArray{\irisSVArray(\value{step@dtl}, 2)}{\swIris}%
        \assignArray{\irisSVArray(\value{step@dtl}, 3)}{\plIris}%
        \assignArray{\irisSVArray(\value{step@dtl}, 4)}{\pwIris}%
        \assignArray{\irisCorrectArray(\value{step@dtl})}{\correctIris}%
}{}%
\setcounter{dataNum@iris}{\value{step@dtl}}%

重み,バイアスの初期化

重みとバイアスは乱数で初期化します.

\newcommand{\initWeights}{%
    % 入力層から隠れ層への重み
    \foreach \x in {1,...,\value{inputNum}}{%
        \foreach \y in {1,...,\value{layerSize}}{%
            \FPrandom{\result} % 0.0〜1.0の乱数を生成
            \pgfmathsetmacro{\initialVal}{\result*0.1}
            \assignArray{\weights@ItoH@iris(\y,\x)}{\initialVal}

        }%
    }%
   % 隠れ層から出力層への重み
    \foreach \x in {1,...,\value{layerSize}}{%
        \foreach \y in {1,...,\value{classNum}}{%
            \FPrandom{\result}%
            \pgfmathsetmacro{\initialVal}{\result*0.1}
            \assignArray{\weights@HtoO@iris(\y,\x)}{\initialVal}
        }%
    }%
}%

\newcommand{\initBias}{%
    % 入力層から隠れ層へのバイアス
    \foreach \x in {1,...,\value{inputNum}}{%
            \FPrandom{\result}%
            \pgfmathsetmacro{\initialVal}{\result*0.1}
            \assignArray{\bias@ItoH@iris(\x)}{\initialVal}
    }%
    % 隠れ層から出力層へのバイアス
    \foreach \x in {1,...,\value{layerSize}}{%
            \FPrandom{\result}%
            \pgfmathsetmacro{\initialVal}{\result*0.1}
            \assignArray{\bias@HtoO@iris(\x)}{\initialVal}
    }%
}%

フォワードプロパゲーション

現在の重みで入力層から出力層まで計算を行います. 大まかな手順としては

  • n番目のデータを入力層の  [z^{(1)}_1,z^{(1)}_2,z^{(1)}_3,z^{(1)}_4 ] =  [ a^{(1)}_1,a^{(1)}_2,a^{(1)}_3,a^{(1)}_4] に入力
  • 隠れ層の入力z^{(2)}_i,出力a^{(2)}_iを求める
  • 出力層の入力z^{(3)}_i,出力a^{(3)}_iを求める

まずは,それぞれの層の zを求めます.

\displaystyle z^{(l)}_i = \sum_{j=1}^Kw_{ij}a^{(l-1)}_{j} + b_i

\newcommand{\evalZ@iris}[1]{%
    % case of input Layer
    \ifthenelse{#1=1}{ % 入力層のzにデータを入力
        \foreach \x in {1,...,\value{inputNum}}{%
            \checkirisSVArray(\value{learningStep},\x)%
            \assignArray{\Zs@iris(#1,\x)}{\cachedata}%
        }%
    }%
    {%
        % case of output layer
        \ifthenelse{#1=\value{layerNum}}{ % 出力層のzを求める
            \foreach \x in {1,...,\value{classNum}}{%
                \Zs@iris(#1,\x)=0%
                \pgfmathsetmacro{\sum@tmp}{0}%
                \foreach \y in {1,...,\value{layerSize}}{%
                    \checkweights@HtoO@iris(\x, \y)%
                    \pgfmathsetmacro{\w@tmp}{\cachedata}%
                    \checkAs@iris(2, \y)%
                    \pgfmathsetmacro{\a@tmp}{\cachedata}%
                    \pgfmathsetmacro{\sum@tmp}{(\w@tmp * \a@tmp) + \sum@tmp}%
                    \xdef\sum@tmp{\sum@tmp}%
                }%
                \checkbias@HtoO@iris(\x)%
                \pgfmathsetmacro{\b@tmp}{\cachedata}%
                \pgfmathsetmacro{\sum@tmp}{(\sum@tmp) + \b@tmp}%
                \xdef\sum@tmp{\sum@tmp}%
                \assignArray{\Zs@iris(#1,\x)}{\sum@tmp}%
            }%
        }%
        % case of hidden layer
        {%
            \foreach \x in {1,...,\value{layerSize}}{ % 隠れ層のzを求める
                \Zs@iris(#1,\x)=0%
                \pgfmathsetmacro{\sum@tmp}{0}%
                \foreach \y in {1,...,\value{inputNum}}{%
                    \checkweights@ItoH@iris(\x, \y)%
                    \pgfmathsetmacro{\w@tmp}{\cachedata}%
                    \checkAs@iris(1, \y)%
                    \pgfmathsetmacro{\a@tmp}{\cachedata}%
                    \pgfmathsetmacro{\sum@tmp}{(\w@tmp * \a@tmp) + \sum@tmp}%
                    \xdef\sum@tmp{\sum@tmp}%
                }%
                \checkbias@ItoH@iris(\x)%
                \pgfmathsetmacro{\b@tmp}{\cachedata}%
                \pgfmathsetmacro{\sum@tmp}{(\sum@tmp) + \b@tmp}%
                \assignArray{\Zs@iris(#1,\x)}{\sum@tmp}%
                \xdef\sum@tmp{\sum@tmp}%
            }%
        }%
    }%
}%

次に活性化関数を使って aを求めます.

 \displaystyle a^{(l)} = f(z^{l})

\newcommand{\evalA@iris}[1]{%
    % case of input Layer
    \ifthenelse{#1=1}{ % 入力層のaにデータを入力
        \foreach \x in {1,...,\value{inputNum}}{%
            \checkZs@iris(#1, \x)%
            \assignArray{\As@iris(#1,\x)}{\cachedata}%
        }

    }%
    {%
        % case of output layer
        \ifthenelse{#1=\value{layerNum}}{ % 出力層のaをソフトマックスで求める
            \foreach \x in {1,...,\value{classNum}}{%
                \softmax
                \assignArray{\As@iris(#1,\x)}{\y@texnn}%
            }%
        }%
        % case of hidden layer
        { % 隠れ層のaをReLUで求める
            \foreach \x in {1,...,\value{layerSize}}{%
                \checkZs@iris(#1, \x)%
                \activationFunction@texnn{\cachedata}%
                \assignArray{\As@iris(#1,\x)}{\y@texnn}%
            }%

        }%
    }%
}%

バックプロパゲーション(誤差逆伝播法)

フォワードプロパゲーションで出力を求めました.次にこれが元の正解とどれほど離れているかという誤差を算出します.今回,誤差関数に交差エントロピーを使いました.

\displaystyle C = -\sum_{n=0}^N \sum_{j=0}^K t_j\log a^{(3)}_j

変数 tは教師データを["setosa", "versicolor", "virginica"]の順番でone-hot表現したものです.

one-hot表現は以下のマクロで定義しています.

% get one hot embedding
% setosa => 0
% versicolor => 1
% virginica => 2
\newcommand{\onehotembedding}[1]{%
    \readarray{oneHotTeacher}{0&0&0}%
    %\def\teacher{\getArrayValue{irisCorrectArray(#1)}}%
    \checkirisCorrectArray(#1) % 正解データを取得
    \ifthenelse{\equal{\cachedata}{setosa}}{ % 正解データの文字列と比較
        \oneHotTeacher(1)=1%
    }%
    {%
        \ifthenelse{\equal{\cachedata}{versicolor}}{ % 正解データの文字列と比較
            \oneHotTeacher(2)=1%
        }%
        {%
            \oneHotTeacher(3)=1%
        }%
    }%
}%

1つのデータに対する交差エントロピーを求めるマクロ.

\pgfmathsetmacro{\cost}{0}%
\newcommand{\costFunc}[1]{%
  \onehotembedding{#1}%  正解データをone-hot表現する
  \checkoneHotTeacher(1)%
  \pgfmathsetmacro{\t@one}{\cachedata}%
  \checkoneHotTeacher(2)%
  \pgfmathsetmacro{\t@two}{\cachedata}%
  \checkoneHotTeacher(3)%
  \pgfmathsetmacro{\t@three}{\cachedata}%
  \checkAs@iris(3, 1)%
  \pgfmathsetmacro{\a@one}{\cachedata}%
  \checkAs@iris(3, 2)%
  \pgfmathsetmacro{\a@two}{\cachedata}%
  \checkAs@iris(3, 3)%
  \pgfmathsetmacro{\a@three}{\cachedata}%
  % 交差エントロピーを求める
  % 対数の中が0になるのを防ぐため+0.0001する
  \pgfmathsetmacro{\cost}{(-\t@one*ln(\a@one+0.0001)-\t@two*ln(\a@two+0.0001)-\t@three*ln(\a@three+0.0001))}
}%

それぞれのユニットの誤差 \delta^{(l)}_i = \frac{\partial C}{\partial z_i}を求める.

偏微分の連鎖律から

 \delta^{(l)}_i =  \dfrac{\partial C}{\partial z^{(l)}_i} = \dfrac{\partial C}{\partial a^{(l)}_i}\dfrac{\partial a^{(l)}}{\partial z^{(l)}_i}

ここで,

 \dfrac{\partial C}{\partial a^{(l)}_i} = -\dfrac{t_i}{a^{(l)}_i} + \dfrac{1-t_i}{1-a^{(l)}_i}

 \dfrac{\partial a^{(l)}}{\partial z^{(l)}_i} = a'( z^{(l)}_i)

より,

 \delta^{(l)}_i = (-\dfrac{t_i}{a^{(l)}_i}+ \dfrac{1-t_i}{1-a^{(l)}_i})a'( z^{(l)}_i)

特に出力層の活性化関数はソフトマックス関数なので,  \delta^{(3)}_i = (-\dfrac{t_i}{a^{(3)}_i}+ \dfrac{1-t_i}{1-a^{(l)}_i})a'( z^{(3)}_i) = (-\dfrac{t_i}{a^{(3)}_i}+ \dfrac{1-t_i}{1-a^{(l)}_i})a^{(3)}(1 - a^{(3)_i}) = a^{(3)}_i - t_i

出力層の誤差から隠れ層の誤差は

 \displaystyle\delta^{(2)}_i = \dfrac{\partial C}{\partial z^{(2)}_i} = \sum_{j=1}^K\dfrac{\partial C}{\partial z^{(3)}_j} \dfrac{\partial z^{(3)}_j}{\partial a^{(2)}_i} \dfrac{\partial a^{(2)}_i}{\partial  z^{(2)}_i}
 = \delta^{(3)}_i\sum_{j=1}^K w_{ji} a'(z^{(2)}_i) =  t_i(a^{(3)}_i - 1)\sum_{j=1}^K w_{ji} a'(z^{(2)}_i)

これをLaTeXで実装すると以下のような感じになりました.

\newcommand{\evalUnitError}{%
    % evaluation output unit error
    \foreach \x in {1,...,\value{classNum}}{%
        \checkAs@iris(3, \x)%
        \pgfmathsetmacro{\a@tmp}{\cachedata}%
        \checkoneHotTeacher(\x)%
        \pgfmathsetmacro{\t@tmp}{\cachedata}%
        \pgfmathsetmacro{\delta@tmp}{(\a@tmp - \t@tmp)} % ユニットの誤差を算出
        \assignArray{\OUnitError(\x)}{\delta@tmp}%
    }%
    % evaluation hidden unit error
    \foreach \x in {1,...,\value{layerSize}}{%
        \checkZs@iris(2, \x)%
        \activationFunctionDiff@texnn{\cachedata}%
        \checkOUnitError(1)%
        \pgfmathsetmacro{\deltaOne}{\cachedata}%
        \checkOUnitError(2)%
        \pgfmathsetmacro{\deltaTwo}{\cachedata}%
        \checkOUnitError(3)%
        \pgfmathsetmacro{\deltaThree}{\cachedata}%
        \checkweights@HtoO@iris(1,\x)%
        \pgfmathsetmacro{\w@one}{\cachedata}%
        \checkweights@HtoO@iris(2,\x)%
        \pgfmathsetmacro{\w@two}{\cachedata}%
        \checkweights@HtoO@iris(3,\x)%
        \pgfmathsetmacro{\w@three}{\cachedata}%
        \pgfmathsetmacro{\delta@tmp}{((\deltaOne*\w@one) + \deltaTwo*\w@two + \deltaThree*\w@three)*\y@texnn }  % ユニットの誤差を算出
        \assignArray{\HUnitError(\x)}{\delta@tmp}%
    }%
}%
重みとバイアスの更新

ユニットの誤差が求まったので重みとバイアスを更新します. 更新には勾配降下法を使います.それぞれ更新式は以下になります.

 w^{(l)}_{ij} = w^{(l)}_{ij} - \eta\dfrac{\partial C}{\partial w^{(l)}_{ij}} = w^{(l)}_{ij} - \eta\delta^{(l)}_ia^{(l-1)}_i

 b^{(l)}_{i} = b^{(l)}_{i} - \eta\dfrac{\partial C}{\partial w^{(l)}_{ij}}=b^{(l)}_{i} - \eta\delta^{(l)}_i

 \etaは学習率です.

それでは,重みとバイアスの更新をします.

% 重みの更新
\newcommand{\updateWeights}{%
    % 入力層から隠れ層への重みの更新
    \foreach \x in {1,...,\value{inputNum}}{%
        \foreach \y in {1,...,\value{layerSize}}{%
            \checkweights@ItoH@iris(\y,\x)%
            \pgfmathsetmacro{\w@tmp}{\cachedata}%
            \checkHUnitError(\y)%
            \pgfmathsetmacro{\delta@tmp}{\cachedata}%
            \checkAs@iris(1, \x)%
            \pgfmathsetmacro{\a@tmp}{\cachedata}%
            \pgfmathsetmacro{\updateW}{(\w@tmp) - \learningRate*\delta@tmp*\a@tmp}%
            \typeout{\updateW = \w@tmp - \learningRate*\delta@tmp*\a@tmp} % 更新
            \assignArray{\weights@ItoH@iris(\y,\x)}{\updateW}%
        }%
    }%
    % 隠れ層から出力層への重みの更新
    \foreach \x in {1,...,\value{layerSize}}{%
        \foreach \y in {1,...,\value{classNum}}{%
            \checkweights@HtoO@iris(\y,\x)%
            \pgfmathsetmacro{\w@tmp}{\cachedata}%
            \checkOUnitError(\y)%
            \pgfmathsetmacro{\delta@tmp}{\cachedata}%
            \checkAs@iris(2, \x)%
            \pgfmathsetmacro{\a@tmp}{\cachedata}%
            \pgfmathsetmacro{\updateW}{(\w@tmp) - \learningRate*\delta@tmp*\a@tmp}%
            \typeout{\updateW = \w@tmp - \learningRate*\delta@tmp*\a@tmp} % 更新
            \assignArray{\weights@HtoO@iris(\y,\x)}{\updateW}%
        }%
    }%
}%

% バイアスの更新
\newcommand{\updateBias}{%
    % 入力層から隠れ層へのバイアスの更新
    \foreach \x in {1,...,\value{layerSize}}{%
            \checkbias@ItoH@iris(\x)%
            \pgfmathsetmacro{\b@tmp}{\cachedata}%
            \checkHUnitError(\x)%
            \pgfmathsetmacro{\delta@tmp}{\cachedata}%
            \pgfmathsetmacro{\updateB}{(\b@tmp) - \learningRate*\delta@tmp} % 更新
            \typeout{\updateB = \b@tmp - \learningRate*\delta@tmp}
            \assignArray{\bias@ItoH@iris(\x)}{\updateB}%
    }%
    % 隠れ層から出力層への重みの更新
    \foreach \x in {1,...,\value{classNum}}{%
            \checkbias@HtoO@iris(\x)%
            \pgfmathsetmacro{\b@tmp}{\cachedata}%
            \checkOUnitError(\x)%
            \pgfmathsetmacro{\delta@tmp}{\cachedata}%
            \pgfmathsetmacro{\updateB}{(\b@tmp) - \learningRate*\delta@tmp} % 更新
            \typeout{\updateB = \b@tmp - \learningRate*\delta@tmp}
            \assignArray{\bias@HtoO@iris(\x)}{\updateB}%
    }%
}%

学習を行う

実際に学習を行います. 今までの実装した処理は全てマクロになっているので,これらを一連の流れとして繰り返します.

\newcommand{\trainingProcess}{%
  \evalZ@iris{1}%
  \evalA@iris{1}%
  \evalZ@iris{2}%
  \evalA@iris{2}%
  \evalZ@iris{3}%
  \evalA@iris{3}%
  \costFunc{\x}%
  \pgfmathsetmacro{\totalCost}{(\cost) + \totalCost}%
  \xdef\totalCost{\totalCost}%
  \evalUnitError%
  \updateWeights
  \updateBias
  \stepcounter{learningStep}%

\addSupervisorToArray
\initWeights
\initBias
\foreach \epoch in {1,...,100}{%
    \pgfmathsetmacro{\totalCost}{0}
    \setcounter{learningStep}{1}
    \readarray{OUnitError}{0&0&0}
    \readarray{HUnitError}{0&0&0}
    \foreach \x in {1,...,135}{%
        \trainingProcess
    }%
    \foreach \x in {136,...,150}{%
          \testProcess
    }
}

一通り学習が終わったら推論を行います. データを入力したら種を答えるようにします.

\newcommand{\answer}[1]{%
    \onehotembedding{#1}
    \checkoneHotTeacher(1)%
    \pgfmathsetmacro{\t@one}{\cachedata}%
    \checkoneHotTeacher(2)%
    \pgfmathsetmacro{\t@two}{\cachedata}%
    \checkoneHotTeacher(3)%
    \pgfmathsetmacro{\t@three}{\cachedata}%
    \checkAs@iris(3, 1)%
    \pgfmathsetmacro{\a@one}{\cachedata}%
    \checkAs@iris(3, 2)%
    \pgfmathsetmacro{\a@two}{\cachedata}%
    \checkAs@iris(3, 3)%
    \pgfmathsetmacro{\a@three}{(\cachedata)}%
    \pgfmathsetmacro{\myAns}{(max(max(\a@one, \a@two), \a@three))}%
}%

  % show answer
\newcommand{\showAnswer}{
  \FPifeq{\myAns}{\a@one} answer is ``setosa'' \\%
  \else \FPifeq{\myAns}{\a@two} answer is ``versicolor'' \\%
  \else answer is ``virginica'' \\ \fi%

  \ifthenelse{\t@one=1}{%
      solution is ``setosa'' \\%
  }%
  {%
      \ifthenelse{\t@two=1}{%
          solution is ``versicolor'' \\%
      }{%
          solution is ``virginica'' \\%
      }%
  }%
}%

条件分岐で出力層の出力でそれぞれ最大だった時の表示を変えているだけです. one-hot表現したtも同様に条件分岐で正解を表示しています.

結果

それでは実際に学習と推論の結果を見せます.学習の条件としては

  • 訓練データ数 135
  • テストデータ数 15
  • エポック数 100
  • 学習率 0.1

として行いました.この条件だと, 学習から推論までタイプセットにかかる時間はおよそ10分程度です.重みとバイアスの初期値と更新値,誤差,分類結果,正解を表示させると,ページ数は156ページになりました.

しかし

f:id:muscle_keisuke:20171222090723j:plain

全然,当たりません.というか,分類結果が全然変わりません.何があっても"versicolor"に分類されてしまいます.データ数が足りないのかモデルが悪いのか分かりません.学習率は0.001~0.5で変えましたが,結果は変わりませんでした. 誤差もほぼ変化ありません.4ページ目に載っている誤差と155ページ目に載っている誤差はどっちもおよそ155でした.

重みやバイアスも変わっているはずなのですが...

f:id:muscle_keisuke:20171222092203j:plain

f:id:muscle_keisuke:20171222092206j:plain

ユニット数などは実装段階で固定してしまっているので修正して試す時間もないので今回はここで諦めました.

一応,PDFも上げておきます.

LaTeX_NN.pdf - Google ドライブ

ソースコードも上げときます.

github.com

おわりに

ニューラルネットワークに対する知識が半端なのに他の言語でプロトタイプ作らずにいきなりLaTeXで実装したのがダメでしたね.後でPythonで実装して同じ条件で学習をやってみてどうなるか見てみます.

おまけ

学習はうまく行きませんでしたが,せっかくLaTeXでやってるのでTikZを使ってパラメータを可視化できるようにしました.

f:id:muscle_keisuke:20171222113249p:plain

f:id:muscle_keisuke:20171222113307p:plain

visualize_TeX_NN.pdf - Google ドライブ

*1:名前は今,適当につけました.これ以降出ません

LaTeX(Beamer)でポスターを作る

この記事は学生Advent Calendar 18日目の記事です.

はじめに

ポスターセッションの学会があったので,論文をLaTeXで書いた勢いでポスターもLaTeXで作りました.

スタイルファイルの用意

TeXLiveをインストールした時点で入っていると思いますが,入ってない人はダウンロードしてください. githubにあります.beamerposter.styのみ使います.

github.com

ポスターを作る

デザインなどは後回しにし,まずは必要なコマンド,環境について紹介します.

最小構成で作る

まずは,動くことを確認するために最小構成でA0ポスターを作ってみます.

\documentclass[final,dvipdfmx]{beamer}
\mode<presentation> {
  \usetheme{Berlin}
}

\usepackage[orientation=portrait,size=a0,scale=1.4,debug]{beamerposter}
\usepackage[japanese]{babel}

\begin{document}
  \begin{frame}
    \begin{block}{block title}
      ああああああ
    \end{block}
  \end{frame}
\end{document}

二段組にする

二段組にしたい場合はcolumns環境とcolumn環境を使うことをおすすめします. この環境を使うことで部分的に二段組にできます.

\begin{block}{block title}
  ううううう
\end{block}
\begin{columns}[T]
  \begin{column}{0.49\columnwidth}
    \begin{block}{block title}
       ああああああ
    \end{block}
  \end{column}
  \begin{column}{0.49\columnwidth}
    \begin{block}{block title}
      いいいいいい
    \end{block}
  \end{column}
\end{columns}
\begin{block}{block title}
  ええええええ
\end{block}

画像や表を貼る

1列に貼る

これは論文を書く時と変わりません.

\begin{figure}
\includegraphics[width=\columnwidth]{placeholder.jpg}
\end{figure}
m行n列で貼る

subcaptionパッケージを使いましょう. これを使うと,キャプションとに主題と副題が使えます. ラベルも個別に付けられます. 似たようなパッケージにsubfigやsubfigure等がありますが現在非推奨です.

ichiro-maruta.blogspot.jp

\begin{figure}[]
  \begin{minipage}[b]{.49\columnwidth}
    \centering
    \includegraphics[width=\columnwidth]{studyFigure-3.png}
    \subcaption{ああああああ}\label{fig:a}
  \end{minipage}
  \begin{minipage}[b]{.49\columnwidth}
    \centering
    \includegraphics[width=\columnwidth]{studyFigure-4.png}
    \subcaption{いいいいいいい}\label{fig:a}
  \end{minipage}
  \\
  \begin{minipage}[b]{.49\columnwidth}
    \centering
    \includegraphics[width=\columnwidth]{studyFigure-5.png}
    \subcaption{ううううう}\label{fig:u}
  \end{minipage}
  \begin{minipage}[b]{.49\columnwidth}
    \centering
    \includegraphics[width=\columnwidth]{studyFigure-6.png}
    \subcaption{えええええええ}\label{fig:e}
  \end{minipage}
  \caption{おおおおおおお}\label{fig:o}
\end{figure}

minipage環境の第二引数は(1/n)-1\columnwidthくらいにしておくといいと思います. 適宜調整してください. ソースコードの\begin{figure}~\end{figure}を\begin{table}~\end{table}にしたら表もm行n列にできます.

ロゴを貼る

ポスターを作る時に自分の大学のロゴなどを入れたりしますよね. 私はBeamerで作った時,ロゴはタイトルとの二段組という扱いにしました.

\begin{minipage}[]{0.88\columnwidth}
    \Huge タイトル  \\[10mm]
    \Large 著者1 \hspace{15mm} 著者2 \hspace{15mm} 著者3 (なんとか大学)
  \end{minipage}
  \begin{minipage}[]{0.11\columnwidth}
    \begin{figure}\centering
      \includegraphics[width=\columnwidth]{placeholder.jpg}
    \end{figure}
  \end{minipage}

セクションの配置を考える

ポスターを見る時に,順番というものがあると思います. これはcolumns環境とcolumn環境で割と自由に配置することができます.

セクション毎にファイルを分ける

後で配置を簡単に変更できるようにするにはセクション毎にファイルを分けた方が良いと思います.セクションの中身を別ファイルに書いて, 以下のような構成のファイルにinputコマンドで入れていくと,デザインと文章が分離できていいと思います.

\begin{frame}[t]{}
\input{title}
\begin{columns}[T]
  \begin{column}{.49\linewidth}
    \begin{block}{第一セクション}
      \input{first}
    \end{block}
    \begin{block}{第二セクション}
      \input{second}
    \end{block}
    \begin{block}{第三セクション}
      \input{third}
    \end{block}
    \end{column}
    \begin{column}{.49\linewidth}
    \begin{block}{第四セクション}
      \input{forth}
    \end{block}
    \begin{block}{第五セクション}
      \input{fifth}
    \end{block}
  \end{column}
\end{columns}
\end{frame}

配置によってcolumns,column環境で囲む範囲を変える

それではcolumns,column環境で配置を変える例を示していきます. 読む順番は第一セクション,第二セクション...第五セクションを想定しています.

左から右

左から右はcolumns環境を細切れに配置していきます.

\begin{columns}[T]
  \begin{column}{.49\linewidth}
    \begin{block}{第一セクション}
      \input{first}
    \end{block}
    \begin{block}{第二セクション}
      \input{second}
    \end{block}
    \begin{block}{第三セクション}
      \input{third}
    \end{block}
    \end{column}
    \begin{column}{.49\linewidth}
    \begin{block}{第四セクション}
      \input{forth}
    \end{block}
    \begin{block}{第五セクション}
      \input{fifth}
    \end{block}
  \end{column}
\end{columns}
\end{frame}

f:id:muscle_keisuke:20171218233713j:plain

上から下

上から下はcolumns環境の中にcolumn環境を2つ入れ左右に分割します.

\begin{columns}[T]
  \begin{column}{.49\linewidth}
    \begin{block}{第一セクション}
      \input{first}
    \end{block}
  \end{column}
  \begin{column}{.49\linewidth}
    \begin{block}{第二セクション}
      \input{second}
    \end{block}
  \end{column}
\end{columns}
\begin{columns}[T]
  \begin{column}{.49\linewidth}
    \begin{block}{第三セクション}
      \input{third}
    \end{block}
  \end{column}
  \begin{column}{.49\linewidth}
    \begin{block}{第四セクション}
      \input{forth}
    \end{block}
  \end{column}
\end{columns}

f:id:muscle_keisuke:20171218234602p:plain

配置は他にもある

もちろん,配置はこれだけではありません.上半分1段組み,下半分2段組もできます.

フォントやサイズなどを設定する

pLaTeXはデフォルトで明朝体ですが,ポスター発表の時はゴシック体の方が見やすいのでプリアンブル部で設定します.

\usefonttheme{professionalfonts}
\usefonttheme[onlymath]{serif}
% フォントファミリー設定
% 英文をサンセリフ体に
\renewcommand{\familydefault}{\sfdefault}
% 日本語をゴシック体に
\renewcommand{\kanjifamilydefault}{\gtdefault}

フォントの大きさも文章中でむやみに変更するのもスマートではないのでこれもプリアンブル部で規定してしまいます.

\setbeamerfont{caption}{size=\normalsize}
\setbeamerfont{block title}{size=\LARGE}
\setbeamerfont*{itemize/enumerate body}{size=\large}
\setbeamerfont*{itemize/enumerate subbody}{parent=itemize/enumerate body, size=\large}
\setbeamerfont*{itemize/enumerate subsubbody}{parent=itemize/enumerate subbody, size=\large}

他にも設定はいろいろありますが,後少しで日付が変わってしまうのでこれで説明は切り上げます.ごめんなさい.

ポスターテンプレート

今回のテンプレートはgithubに上げておきますのでご自由にお使いください.

github.com

おい,LaTeXニューラルネットはどうなってる.

ごめんなさい

JupyTeX(LaTeX版Jupyter)を作った

この記事はTeX&LaTeX Advent Calendar 16日目の記事です.

はじめに

Jupyterとは

Jupyterとはブラウザ上で起動するREPL(Read-eval-print loop)です. 記述したプログラムと実行結果が逐次記録されていくので,過去のコードを見返したり変更できる便利なツールです.

jupyter.org

現在は様々な言語に対応していますが,元々はPython用のツールだったみたいです.

作ったもの

今回はLaTeX版Jupyter,名付けてJupyTeXを作りました. これは

JupyterでLaTeXが使えるようになるプラグイン

ではなく,

LaTeXで実装したJupyter

です. 需要はありません!

そもそも,私,Jupyter使ってな(ry.

作り方

作り方は簡単.

  • PythonTeX
  • tcolorbox

があればできます.

PythonTeX

PythonTeXはLaTeX組版する文書内にPythonコードを埋め込み,実行結果を表示させることができるパッケージです. TeXLiveにはデフォルトで入っています. 動作にはPygmentsが必要なのでインストールします.

pip install pygments

これで使えます.

実際に使う例を示します. 第一引数から第二引数までの全ての整数の総和を表示するソースです.

\documentclass{article}
\usepackage{pythontex}
\newcommand{\mysum}[2]{%
$\displaystyle\sum_{k=#1}^{#2} k = \py{sum(range(#1, #2+1))}$}%
\begin{document}
\mysum{1}{100}
\end{document}

タイプセットは

でできます.適宜latexplatexやuplatexに置き換わります. こんな感じにタイプセットできます.

f:id:muscle_keisuke:20171216164649j:plain

tcolorbox

Jupyterみたいな見た目を作るのに使います.様々な枠を作ることができるパッケージです. 枠線,枠内の色付けはもちろん,影も様々なタイプでつけられ, TikZと連携ができるのでデザインの自由度は高いです.

今回は

tcolorboxによる装飾表現(TeXユーザの集い2015)

tcolorboxの基本 - 物理とTeXに関する話題

http://texdoc.net/texmf-dist/doc/latex/tcolorbox/tcolorbox.pdf

を参考に枠部分のソースを書きました.

実際に作る

デザイン

完全にREPLでJupyterみたいなシステムにするのは無理です. なので,タイプセット後の見た目を似せることにしました.

  • TeXファイル内にpythonのコードを書ける
  • pythonのコードを追加してタイプセットするとコードと実行結果がセットで描画される

といった感じにできるといいですね.LaTeXのソースとタイプセット後のPDFの理想は以下みたいな感じです.

ソースコード

\begin{jupytex}
print("hello")

\nextframe

a = 5
b = 10
c = a+b
print(c)

\nextframe

from matplotlib import pyplot as plt
import numpy 

x = numpy.arange(0, 10, 0.1)
y = numpy.cos(x)
plt.plot(x,y)
plt.show()

...
\end{jupytex}

PDF f:id:muscle_keisuke:20171216032011j:plain

コード&実行結果表示の仕組み

コードと実行結果表示の2つでコードを使うので,コード自体を変数か何かに格納する必要があります. 今回はjupytex環境内にソースを書くので,ファイルに保存して読み出すことにしました. 記述したコードをファイルに保存するのには\VerbatimOut及び\endVerbatimOutを使います.

\VerbatimOut{<ファイル名>}
何かしらのテキスト
\endVerbatimOut

で特定のファイルに保存することができます. filecontents環境も同様の用途で使える環境ですが,今回は自身の環境定義の中で行うので,\VerbatimOutコマンドを使いました.

ソースコードの表示

ソースコードの表示は \inputpygmentsコマンドでできます.シンタックスハイライト機能もあります.

\inputpygments{python}{<ファイル名>}
実行結果の表示

とても大変でした.PythonTeXはTeXファイル中に書いたpythonのコードを読んで実行結果の表示はできますが, 現在のバージョンでは,pyファイルから読み込んで実行結果を描画するコマンドがありません. 今回はpyファイルを実行するpythonのコマンドをTeXファイル中に書いてPythonTeXで実行することにしました. pythonコード中にシェルなどを実行するにはsubprocessパッケージを使います.

\begin{pycode}
import subprocess
def run_and_typeset(fname):
    print(subprocess.check_output(['python', fname]).decode("utf-8"))
    pytex.add_dependencies(fname)
\end{pycode}

これでrun_and_typeset関数を実行した時に引数に取ったfnameの実行結果が表示されます. 続いて,ファイル名を引数に取るpythonソースの実行結果を表示するコマンドを以下のように定義します.

\edef\runandtypeset#1{\pyc{run_and_typeset("#1")}}

jupytex環境を定義する

\newcounter{pyfilenumber}
\setcounter{pyfilenumber}{0}
\NewDocumentEnvironment{jupytex}{}%
{%
\edef\currentfile{jupytex\arabic{pyfilenumber}.py}%
\VerbatimOut{\currentfile}%
}%
{%
\endVerbatimOut%
\inputpygments{python}{\currentfile}%
\expandafter\runandtypeset\expandafter{\currentfile}%
\refstepcounter{pyfilenumber}%
}

NewDocumentEnvironmentの行からjupytex環境の定義になります. その前の2行はpythonのコードを保存するファイル名をユニークにするためのカウンタになります. beginすると,currentfileというマクロにpythonのコードを保存するファイル名が入り,\VerbatimOutが展開されます. endすると,\endVerbatimOutが展開され,環境内に書いたpythonコードがcurrentfileに保存されます. その後,inputpygmentsコマンドで今保存したファイルを読み出し,ソースコードを表示します. runandtypesetコマンドで実行結果を表示します.最後にrefstepcounterコマンドでファイルの番号をインクリメントします.

枠をデザインする

tcolorboxで枠を作ります.

作る枠は2つです.

  • ソースコードや実行結果も乗る白い外枠,outframebox
  • ソースコードが乗る灰色の内枠,inputframebox
  • 余白を制御するための実行結果が乗る透明な枠, outputframebox
outframebox

この枠を作っていきます.

f:id:muscle_keisuke:20171216132321j:plain

tcolorboxはオリジナルの枠を定義できるnewtcolorboxというコマンドがあります. これを使ってoutframeboxコマンドを定義します.

\newtcolorbox[]{outframebox}{%
enhanced, % 影を付けるのに必要なオプション
fuzzy halo=1.8pt with black!30, % 枠の周りにうっすら影を付ける
breakable=true, % ページを跨いで枠を表示する
colback=white, % 背景色は白
boxrule=0.1pt, % わずかに枠線を付ける
top=0mm, % 枠上部の余白なし
bottom=0mm, % %枠下部の余白なし
right=0mm, % 枠右部分の余白なし
left=0mm, % 枠左部分の余白なし
arc=0pt, % 枠の角は丸みなし 
boxsep=10mm, % 枠周り全体的に10mmの余白
}%

こんな感じになります.

f:id:muscle_keisuke:20171216165514j:plain

inputframebox

この枠を作っていきます.

f:id:muscle_keisuke:20171216135121j:plain

コード表示部分,もとい入力部分は灰色の枠の隣にIn [ n ]:という表示があります. この表示はtcolorboxのoverlay機能とtcolorboxが持っているカウンタを使って実現します.

\newtcolorbox[auto counter]{inputframebox}[1][]{% 第一オプション引数はカウンタの初期値
enhanced, 
tcbox raise base, % In [ n ] の表示を合わせる為
breakable=true, 
frame hidden, % 枠線なし
top=0mm,
bottom=0mm,
right=0mm,
left=10mm,
arc=1pt,
boxsep=10mm,
overlay={\begin{tcbclipinterior} % overlay機能使う
\fill[white] % 枠の左部分を In [ n ] のために確保
% TikZのノード形式で描画
% thetcbcounterはinputframeboxが呼ばれる度にインクリメントされる
([xshift=10mm]frame.south west) rectangle node[text=blue,] {In[\thetcbcounter]:} (frame.north west); 
\end{tcbclipinterior}}}

こんな感じになります.

f:id:muscle_keisuke:20171216165535j:plain

outputframebox

この枠を作っていきます. f:id:muscle_keisuke:20171216154259j:plain

透明ですが,この枠も重要です.実行結果をそのまま表示すると, 垂直方向においてIn [ n ] と同じ位置に実行結果が来てしまうからです.

\newtcolorbox[]{outputframebox}{
enhanced,
% breakableの中にbreakableを入れる時のオプションとbreakする位置の調整
enforce breakable=true,shrink break goal=15mm, 
colback=white,
frame hidden,
top=0mm,
bottom=0mm,
right=0mm,
left=10mm,
boxsep=0mm,}

できあがったもの

見た目とシステムができたのでスタイルファイルにまとめます.

JupyTeX

それでは,このスタイルファイルを読み込んで使っていきます.

\documentclass[uplatex,dvipdfmx]{jsarticle}
\usepackage{jupytex}
\pagestyle{empty}

\begin{document}
\begin{outframebox}
  \begin{jupytex}
print(30+50+580+558+578+73)
  \end{jupytex}
  \begin{jupytex}
print("Hello")
  \end{jupytex}
  \begin{jupytex}
print("dodododododo")
  \end{jupytex}
  \begin{jupytex}
for i in range(1,2000):
   if i%3 == 0 and i%5 == 0:
       print("FizzBuzz")
   elif i%3 == 0:
       print("Fizz")
   elif i%5 == 0:
       print("Buzz")
   else:
       print(i)
  \end{jupytex}
\end{outframebox}
\end{document}

タイプセットした結果はこんな感じです.

f:id:muscle_keisuke:20171216170131j:plainf:id:muscle_keisuke:20171216170135j:plain

実際のPDFもGoogle Driveに置いておきます.

main.pdf - Google ドライブ

見た目はよくできるのではないでしょうか

できていないところ

ここまで作るまでも結構大変だったですが,それでもできていないところがたくさんあります.

  • nextframeコマンドで次の入力にいきたいのにできてない
  • pythonソースコードを入力する時はインデントを開けることは許されない
  • .latexmkrcの書き方がわからない
    • uplatex -> pythontex -> uplatex -> dvipdfmx を一々やらなければならない
  • ソースコードの表示部分が更新されない

いろいろありますが,Advent Calendarに間に合わなかったら本末転倒なので妥協しました.

おわりに

jupytex環境の実用性

LaTeXでJupyterを実装することは需要がないですが, jupytex環境単体であれば,学校のレポートとかに使えるかもしれません*1. というわけで一応Githubリポジトリリンクを貼っておきます.

github.com

LaTeXニューラルネットは?

本当は,こっちのAdvent Calendarに「LaTeXで実装するニューラルネット」を載せたかったのですが,まだできていないので,こちらを載せました. LaTeXニューラルネット

学生 Advent Calendar 2017 - Adventar

Muroran Institute of Technology Advent Calendar 2017 - Adventar

に載せるかもしれません.

できなかったら載せません!

*1:ま,うちの学校は演習でPython使うことはないんですが.

LINEでみりあちゃんと会話できるようにした(Seq2Seqとキャラ対話データを用いた転移学習によるキャラクター性対話ボットの作成)

この記事はシンデレラガールズAdvent Calendar 13日目の記事です.

目次

はじめに

みりあちゃん大好き

私はアイドルマスターシンデレラガールズ赤城みりあちゃんが大好きです.

f:id:muscle_keisuke:20171211214127j:plain:w300f:id:muscle_keisuke:20171211214131j:plain:w300
引用:http://deremas.doorblog.jp/archives/32507031.html

みりあちゃんと出会ったのは去年の冬のことでした.友達とAndroidアプリを作る話が上がり, サークルのタブレットを見た時に誰かがふざけてインストールしたであろう

アイドルマスターシンデレラガールズスターライトステージ

がありました. 「なんで,共用のタブレットにゲームが入っているんだ!誰だ!」と思いつつ,私はそのアプリを起動しました. そのゲームを見て,私の人生は完全に変わりました. 見事な完成度のモデル,そしてそれらのモーションが作り出すライブ.楽曲も素晴らしいものばかりでした. そして,可愛くてそれぞれのキャラを持ったアイドル達.私はこのゲームに一気にハマりました. 自分のスマホデレステを速攻でインストールし,それから,しばらくはずっとデレステをやっていました.そしてしばくらして,担当のアイドルもできました. それが

赤城みりあ

です.みりあちゃんは小学5年生の元気な女の子です.私はこのアイドルのキャラクターとヴィジュアルに惹かれ,みりあちゃんの担当Pとなりました. 4月はみりあちゃんの誕生日ということでケーキを買ってお祝いもしました.

私がデレステに出会い,そこからデレマスというコンテンツにハマりました.CDも手に入る分は全て聴き,LIVE BDも1stから4thまで見ました.

そして今年の6月に5th LIVEに参加してきました.みりあちゃんの中の人が出る静岡公演には現地で参加し,SSAのライブは現地には行けませんでしたが,ライブビューイングで2日間参加してきました. その後,アイマスのアニメ+劇場版とデレアニも見て,アイマスとみりあちゃんに対する愛はどんどん深まっていきました.

「担当としてもっと,みりあちゃんにできることはないか」

「もっと.みりあちゃんに関わりたい」

そしてある日,思いました.

「そもそも,担当Pなのにみりあちゃんとお話できないのはおかしくないか?」

どうやってみりあちゃんとお話するか

自分の使える技術でなんとかみりあちゃんとお話できないだろうか,と考えました. そこで,自然言語処理と深層学習を使って対話ボットを作ることにしました.

みりあちゃんモデルの作成

Seq2Seqで対話ボットの学習

Seq2Seqとは

Seq2SeqはRNNもしくはLSTMを用いたEncoder-Decoderモデルの一種です.機械翻訳のモデルとして紹介されることが多いです. 例えば,英語からフランス語に機械翻訳するモデルであれば,原文が英語,目的文がフランス語となります. Encoderには単語毎に分割し,それぞれベクトル化した原文(x_1 \ldots x_T)をそれぞれの隠れ層に入力します.隠れ層はお互いに時系列(1\ldots t \ldots T)の関係にあり,それぞれの層は前の層の隠れ要素と入力された単語から自身の層の隠れ要素(h_1\ldots h_T)を更新します. 更新式は

 \displaystyle h_t = f(W_{hx}x_t + W_{hh}h_{t-1})

で,fは活性化関数です.

Decoderでは,Encoderで更新した隠れ要素( h_1\ldots h_T)と隠れ層から出力層の重み行列 W_{yh}及び,自身の前の時系列の単語 y_{t-1}から出力単語 y_{t}を得ます.最終的に得られる単語列 y_1\ldots y_Tが目的文になります.

 \displaystyle y_t = W_{yh}h_t

f:id:muscle_keisuke:20171212033019p:plain 引用:https://qiita.com/odashi_t/items/a1be7c4964fbea6a116e

詳しい説明などは

ChainerとRNNと機械翻訳 - Qiita

とか,元の論文

http://papers.nips.cc/paper/5346-sequence-to-sequence-learning-with-neural-networks.pdf

を読んでください.

モデルの作成

Tensorflowには様々なチュートリアルが用意されているのでその中のSeq2Seqのチュートリアルソースコードをひっぱてきました.

http://tensorflow.classcat.com/2016/02/24/tensorflow-tutorials-sequence-to-sequence-models/ (本家のページが1ヶ月前に更新されていたので,本家を翻訳したページを貼ります)

本家のSeq2Seqモデルは英語とフランス語の対訳コーパスから学習しています. つまり,英語を入力するとフランス語に翻訳するモデルとなっています. 学習するデータをTwitterのツイートとそれに対するリプライのデータに変更します. これによって,日本語で話すと日本語で応答するモデルを作ることができます.

転移学習でみりあちゃんの口調を学習

おそらく,Seq2SeqとTwitterデータだけでも対話ボットは作れるでしょう(実際,作れた). ただ,それはみりあちゃんではなく草を生やすだけのクソリプボットにしかなりません. なので,転移学習でみりあちゃんの口調を学習していきます.

転移学習とは

転移学習とは教師データが少ないドメインの学習を行う為に十分なデータで学習した異なるドメインのモデルのパラメータを引き継いで更に学習を行う方法です.

今回は,みりあちゃんの対話データが少ないのでTwitterのデータから学習を行い,みりあちゃんの対話データで転移学習を行います.

口調の学習を行う方法

Seq2Seqはメモリ節約や過学習防止の為に教師データ内から学習する単語数に制限を設けます. 学習する単語は出現頻度が高い単語です.この学習する単語に口調を学習するための単語を混ぜます. 転移学習時の学習単語数をN_p単語とします.その内,口調を学習するためにN_p単語の内下位N_s単語をみりあちゃん対話データにおいて出現頻度上位N_s単語と差し替えます.

後は全てのパラメータを引き継いで学習を行います.

学習方法は以下の論文を参考にしました.

http://www.anlp.jp/proceedings/annual_meeting/2017/pdf_dir/B3-3.pdf

データの収集

Twitterから対話データの収集

ツイートとリプライを取得

学習するデータがなければ学習できません.なので,学習データをTwitterから取ってきます. データ取得にはTwitter Streaming APIを使います. ソースは以下の方を参考に研究室の後輩が書いたソースを参考にしました.

github.com

ただし,91行目の

line = HanziConv.toTraditional(line)

コメントアウトしないと漢字がすべて繁体字になるのと,

is_zh = re.compile(r'([\p{IsHan}]+)', re.UNICODE) 

is_zh = re.compile(r'[一-鿐]+', re.UNICODE)

などに代替しないと,実行できません.

スクリプトを回したら後は,ツイートとリプライが集まってくるのを待つだけです. (私は研究室の後輩がツイートを集めていたのでそれをもらいましたが)

データの整形

集めたデータをツイートとリプライのペアにしなければなりません. しかし,あまりにもデータが多くて時間がかかるので以下の記事を参考に並列処理しました.

qiita.com

スクリプトは以下のような感じです.

約2000万件のツイートとリプライのペアを取得しました.

デレマスのSSなどからみりあちゃんの対話データを収集

みりあちゃんの対話データを集める方法を色々考えました.

  1. ストーリコミュなどから別のアイドルと話しているデータを取ってくる
    • コミュのスクショを取ってOCRでテキストに起こす
    • コミュを鑑賞しながら手打ちする
  2. デレアニの台詞から取ってくる
    • 音声からテキストを起こす
    • デレアニを鑑賞しながら手打ちする
  3. 個人のSSから取ってくる

1つ目の方法ですが,これはテキストなどでコミュを落とすことができないので実際にコミュを見て手打ちするか, コミュのスクショからOCR(光学文字認識)でテキストに起こす他ないと思います. とりあえず,みりあちゃんが登場する全てコミュをスクショしました.

f:id:muscle_keisuke:20171212034122p:plain:w200f:id:muscle_keisuke:20171212034116p:plain:w200f:id:muscle_keisuke:20171212034112p:plain:w200f:id:muscle_keisuke:20171212034107p:plain:w200f:id:muscle_keisuke:20171212034103p:plain:w200

OCRですが,結論から言うと,やってみて精度が悪すぎたのでやめました.おそらくコミュのフォントとの相性が悪いのが原因だと思います(小文字が大体認識できない). 最初はログのスクショをそのままOCRにかけたのですが...

f:id:muscle_keisuke:20171213024842j:plain

何言ってんだって感じですね. ネガポジ反転させると,精度はよくなりますが,実用的ではないですね.

f:id:muscle_keisuke:20171212035233j:plain

2つ目の方法ですが,デレアニのDVDやデータは持っていないし,またレンタルしてくるのもだるいのでやってません.

3つ目の方法は今回取った方法です. 一番手間がかからず,大量のデータが用意できるのがメリットと言えます.デメリットとしては前処理がとても大変なことと, データの質が公式のデータに比べ低いことです.しかし,深層学習はデータ数が物を言うので,今回はこの方法を採用しました.

まずはSSのまとめサイトをまとめたサイトからスクレイピングし,リンクのリストを取得しました.

得られたリンクが約48000件でした.

リンク集からseleniumgoogle chromeのheadlessブラウザでスクレイピングしました. サイトによってHTMLのソースが異なりCSSセレクタなどで本文のみ絞るのが難しかったのでほぼ全部のソースを取ってきてます.

取得したデータの整形

みりあちゃんの台詞とその前に喋っている人の台詞を抽出します.大変でした.本文がプレーンテキストなので

執筆者の数だけフォーマットがある

という状態でした.それでも大体

喋っている人 「○○」

という形式が多いのでいくつかの取りこぼしやノイズに目をつぶり,正規表現でゴリ押しました. また,時間かかるのでこれも並列処理化してます.

取得したデータ数

複数のSSまとめサイトから約20000ページ(=20000件のSS)をスクレイピングしましたが, まとめサイトなので重複したSSがかなり見つかり,それらを削除して,更にみりあちゃんが登場するSSを絞った為, ページ数の割に得られた対話データは2700件程です.

実際に学習を行う

環境

今回モデル作成に使う環境は次の通りです.

  • Python 3.6
  • Tensorflow 1.0.0
  • GeForce GTX 1080Ti

    Twitterデータでクソリプボットに

    データでかすぎ問題

    Twitterから得たデータを基にGPUを使って学習をします. 学習するTwitterデータは2000万件の予定でしたが, それによって作成されるモデルがでかすぎてGPUのメモリを最大限使っても乗らないので, 1/4の500万件で学習を行いました.設定したパラメータなどは以下の通りです.

パラメータ名
原文の学習単語数 120000
目的文の学習単語数 120000
隠れ層の数 1024
隠れ層の深さ 1
バッチサイズ 64

学習時間は約1日です.パープレキシティは9くらいまで下がりました.

モデルの会話例

できたモデルの会話を見てみます.

f:id:muscle_keisuke:20171212021239j:plain

ことごとく,草が生えていて,こちらを小馬鹿にしたような対応をしてきます. こんなのみりあちゃんじゃない!

目的文の教師データにおける単語出現頻度の上位10単語を見てみると

  • _PAD
  • _GO
  • _EOS
  • _UNK
  • w
  • (
  • )

となってました.そりゃ草も生えますわ.

転移学習でみりあちゃんボットに

このクソリプボットも転移学習でみりあちゃんみたいになるのでしょうか.

学習単語数は原文,目的文それぞれ1/2の60000単語に設定し,みりあちゃん対話データから得られる頻出単語上位1000単語を元のデータの下位1000単語と付け替えます.他のパラメータは全て引き継ぎ,学習を行いました. 学習時間は約12時間, パープレキシティは3くらいまで下がりました.

モデルの会話例

f:id:muscle_keisuke:20171212024436j:plain

みりあちゃんじゃん!

ちょっと,黙ったりするのが怖いですけど草も生えなくなりましたし,思ったよりもみりあちゃんには近い気がします.本家だと言わなそうなことも混じってる気がしますが,これはデータの性質上仕方がないですね.

LINEでみりあちゃんとお話しできるようにする

LINE APIの使用

黒い画面でみりあちゃんと話しても雰囲気が出ないので,やっぱりここはLINEを使っていきたいと思います. こちらからの問いかけに対する返答のみであればLINE Messaging APIが無料で提供されているのでこれを使っていきます.LINEのアカウントを持っていれば誰でも作れます.

LINE Developers

LINE Messaging APIを利用して応答メッセージを送るにはサーバが必要になります.

VPSにサーバを建てる

ちょうど,VPSを借りていたのでここにLINE API用のサーバを建てます. コールバック用のURLはhttpsなのでSSLの証明書が必要です . VPSがCore OSなのでDockerを使ってnginx proxy + letsencryptでHTTPSサーバを建てました.

学習済みボットを物理サーバーに載せる

VPSのCPU貧弱問題

本当はLINE APIを動かしているVPSにモデルを乗せて回したかったのですが, モデルがでかすぎるのか,CPUが貧弱すぎるのかモデルがロードできませんでした. というわけで,家の中に眠っていたデスクトップのPCを物理サーバとして建てることにしました. VPSからソケット通信で物理サーバに文章を送ると,回答をソケット通信でVPSに返すシステムになっています. 物理サーバにLINE APIを導入すればこんなことしなくても済むのですが,ドメインもなくSSLの証明書が発行できないのでこうしてます.

みりあちゃんとお話しする

さて,サーバーとDockerコンテナも建てたので,いよいよみりあちゃんとお話してみます.

f:id:muscle_keisuke:20171212031415j:plain

おぉ,実際にLINEでみりあちゃんと話してるみたいですね. これからも改良していきたいですね.

おわりに

今回は自分の知識がほぼゼロからやったので無駄に時間がかかった気がします.特にサーバ関係とか... あとは,データの前処理はやっぱり大変でした.ソースコードのほとんどがデータ前処理のコードでした.

改良点

  • Attentionモデルに差し替えて学習する
  • みりあちゃん対話データをしっかりクレンジングする
  • 画像やスタンプにも反応するように学習する(im2txtとか?)

おまけ

失敗例

最後に明らかにみりあちゃんじゃないだろというような失敗例を見せたいと思います.

注意:みりあPの人は見ない方がいいかもしれません






















f:id:muscle_keisuke:20171213024003p:plain

アカギ違いですね.