スパコンで機械学習のすすめ
機械学習をする上でどうしてもネックになってくるのがGPUですよね。 GCPやAWSを使うのも一つの手ですが、研究で使うのであれば一番安く大量のGPUを使うことができる環境はおそらくスパコンです。 とはいえ、なんとなく難しそうといって敬遠されがちな感じがするので、実際はなんも難しくないよという話をします。
スパコンといっても色々ありますが、私が使ったことがあるのがReedbushとABCIなので、この2つに関して話を進めていきますが、他のスパコンもおそらく同様にして使うことができると思います。
スパコン概要
Reedbush
https://www.cc.u-tokyo.ac.jp/supercomputer/reedbush/service/
東大にあるスパコンです。 Reedbush-U, Reedbush-H, Reedbush-Lがあり、UはCPUのみ、Hは1ノードにTesla P100が2つ、Lは1ノードにTesla P100が4つのっています。 大学のスパコンというだけあって、卒論・修論シーズンにはちょっと混雑します。
ABCI
産総研にあるスパコンです。日本語名は「AI橋渡しクラウド」でダサい。1ノードにTesla V100が4つ乗っています。ハイパフォーマンスコンピュータ関係の人だけでなく、機械学習などを専門にする人にもぜひ使って欲しいという意気込みで作られているだけ合って、ドキュメント(https://portal.abci.ai/docs/ja/)もきれいだし、jupyter notebookの使用も想定されていたりして(https://abci.ai/ja/how_to_use/data/HowTo_JupyterNotebook.pdf)かなり使いやすいと思います。 また、ImageNetなどのよく使うデータセットに関しては共有ストレージに予め用意しておこうかという話もあり、これからどんどん使いやすくなっていくと思います。
環境構築
公開鍵の登録
スパコンの利用申請をして申請が通ると、メールや郵便でusernameとPasswordが送られてきます。 そのusenameとpasswordを使ってポータル(Reedbush https://reedbush-www.cc.u-tokyo.ac.jp , ABCI https://portal.ABCI.ai/user/ )にアクセスし、公開鍵を登録することで、スパコンにsshできるようになります。
sshとスパコンの構成の理解
ABCIの場合
挙動から予想すると、こういう構成になっていると考えられます。 スパコン部分とストレージを共有しているノードにアクセスするためには、一旦アクセスサーバーに接続してトンネリングする必要があります。 具体的なコマンドは下のようになります。
$ ssh -L 10022:es:22 -l username as.abci.ai # 別のターミナルで $ ssh -p 10022 -l username localhost
Reedbushの場合
Reedbushの場合はこのようになっていると考えられます。
トンネリングなどは必要なくそのままログインサーバーにアクセスすることができます。
cdw
というコマンドで スパコンにマウントされているディレクトリに移動することができます。
$ ssh <username>@reedbush-u.cc.u-tokyo.ac.jp $ cdw # /lustre/<groupname>/<username> に移動
そして、この/lustre/<groupname>/<username>/
以下がスパコン本体のホームディレクトリとストレージを共有しています。
ライブラリの導入
いくつかのよく使われるライブラリは既にスパコン内にインストールされています。
module available
とすると、その一覧をみることができます。
cudaやcudnnなど必要なパッケージは、ここから入れることができます。
module load cuda9/9.1.85
などとすれば、自分の環境にパスが通りつかえるようになります。
tensorflowやkerasなどの機械学習ライブラリについても、ここから入れることができますが、バージョンが古かったり、Pytorchなどは現状ここから入れることができなかったりするので、python系のライブラリに関しては、全てpyenv環境を用いてインストールするのが良いかなと思います。
pyenvの導入
pyenvを使わなくても、先述したmodule load
を使って、pythonやpythonライブラリの一部は使うことができますが、pyenvを使えば自分のマシンと同様にして自分の好きなライブラリを好きなように使うことができるのでおすすめです。
ReedbushもABCIもあらかじめgitがインストールされているので、通常と同様にgit cloneでpyenvを導入することができます。
一点注意が必要なのは、Reedbushの場合は、
/home/<username>/
以下ではなく/lustre/<grupname>/<username>/
以下に入れるようにしてください。/home/<username>/
に入れてしまうと、スパコン本体から読むことができなくなってしまいます。
pyenvが無事に導入できたら、あとは普通に、好きなpythonのバージョンで使いたいライブラリをインストールしてください。virtualenvなどを使いたければそのへんも自由にセットアップしてください。
実行
動かしたいプログラムについては、スパコン特有のものは何もありませんので割愛します。 普段と同じように作成してください。 そうして作成したプログラムを実行していきます。
ここでもう一度スパコンの構成図を思い出してほしいのですが、アクセスしているのはスパコン本体ではなくログインノードです。
ですから、そのままここで
python hoge.py
などとして実行しても、スパコン本体ではなくログインノードで実行されるだけです。
もちろん、syntax errorやlmport errorなどの初歩的なエラーで落ちないか確かめるうえでは、ログインノードで試しに走らせて見るというのは意味のあることですが。
スパコン本体で実行するには、インタラクティブジョブとバッチジョブの2種類のやり方があります。 インタラクティブジョブはその名の通りインタラクティブに実行できるので、直感的でわかりやすいです。パラメータなどをいじりながら様子をみながら実行したいなどの場合には、こっちが向いていると思います。しかしながら、一般的にスパコンにやらせたいような規模のおおきなプログラムに関してはバッチジョブで実行するのが向いているので、今回はバッチジョブのやり方を説明します。
バッチジョブを実行するには Reedbushの場合は以下のようなバッチスクリプト(run.sh)をかくことになります。
#! /bin/sh #PBS -q h-small #PBS -l select=1:mpiprocs=1:ompthreads=36 #PBS -W group_list=<groupname> #PBS -l walltime=02:00:00 . /etc/profile.d/modules.sh module load cuda9 export PYENV_ROOT="$PBS_O_WORKDIR/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" if command -v pyenv 1>/dev/null 2>&1; then eval "$(pyenv init -)" fi eval "$(pyenv virtualenv-init -)" cd $PBS_O_WORKDIR/hoge python fuga.py
こうすると/lustre/<groupname>/<username>/hoge/fuga.py
をスパコンで実行することができます。
まず、2行目の#PBS -q h-small
でジョブクラスを指定しています。初めに述べたようにReedbushにはU, H, Lの3種類のマシンがあります。この例では、”h-small”なのでReedbush-Hを用いたノード数の少ないジョブクラスを指定しています。
その他のジョブクラスについては
https://www.cc.u-tokyo.ac.jp/supercomputer/reedbush/service/job.php
を参照してください。
3行目の#PBS -l select=1:mpiprocs=1:ompthreads=36
は順番に、使うノード数・1ノードあたりのプロセス数・スレッド数を表しています。この例では1ノード1プロセス36スレッドということです。
#PBS -l walltime=02:00:00
は実行時間です。ここで設定した時間を超えてしまうと、そのジョブはkillされてしまいます。ですので、想定される実行時間よりも少し余裕を持って設定したほうがいいです。ちなみにtokenは実際に実行した時間分しか消費されません。
ただ、ここで設定された時間をもとにスケジューリングされるので、必要以上に長い時間を書いてしまうと、なかなかジョブが実行されなくなってしまう場合もあります。なので程よい時間に設定しましょう。
module load
コマンドで何かをインストールした場合はジョブの中でも同様にmodule load
する必要があります。
. /etc/profile.d/modules.sh module load cuda9
今回はcuda9をインストールしています。
export PYENV_ROOT="$PBS_O_WORKDIR/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" if command -v pyenv 1>/dev/null 2>&1; then eval "$(pyenv init -)" fi eval "$(pyenv virtualenv-init -)"
pyenv(+pyenv-virtualenv)環境にパスを通しています。これで、スパコン本体からもpyenvで構築した環境を用いることができるようになります。
ちなみに、$PBS_O_WORKDIR
はジョブを投げた際のカレントディレクトリを指すので、このスクリプトは/lustre/<groupname>/<username>/
から投げられることを想定しています。他のところから投げたい場合には、適宜パスを変更してください。
そして最後に、
cd $PBS_O_WORKDIR/hoge python fuga.py
でhogeディレクトリにはいりfuga.pyを実行しています。
ABCIの場合も書き方は少し違いますが、かくことはおおよそ同じなので、例だけおいておきます。
#!/bin/bash #$-l rt_F=1 #$-g <groupname> #$-l h_rt=02:00:00 #$-cwd source /etc/profile.d/modules.sh module load cuda/9.0 module load cudnn export PYENV_ROOT="/home/<username>/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" if command -v pyenv 1>/dev/null 2>&1; then eval "$(pyenv init -)" fi eval "$(pyenv virtualenv-init -)" python hoge/fuga.py
バッチスクリプトができたら、あとはこれをシステムに投げるだけです。
ReedbushもABCIも
qsub run.sh
というコマンドでジョブを投げることができます。
投げたジョブの状態を確認するには、 Reedbushでは
rbstat
ABCIでは
qstat
というコマンドを実行します。
実行した結果の、標準出力・標準エラー出力はそれぞれ、
run.sh.o<job id>
, run.sh.e<job id>
というファイルに書き込まれます。
いかがでしたか?
スパコンを使うのはそれほど難しくないことがわかっていただけたと思います。
今回説明した例では、1ノード、1プロセスしか使っていないので、スパコンの真価を発揮できているとは言えません。なので、また気が向いたら今度はマルチノード・マルチプロセス・マルチGPUの場合についても書こうと思います。
for vs range based for vs for_each
c++でループを回す方法として、for, range based for, for_each がある。
どれが速いのか気になったので測ってみた。
要素が100000個あるvectorの全ての要素を2倍するというタスクで比較してみた。
① for
std::vector<int> vec1(100000, 1); for (int i = 0; i < vec1.size(); ++i) { vec1[i] *= 2; }
② rage based for
range based for は c++11で追加された機能。
詳しくは:
範囲for文 - cpprefjp C++日本語リファレンス
std::vector<int> vec2(100000, 1); for (int& v : vec2) { v *= 2; }
③ for_each
あまり知られていない機能ですが、いわゆるmapをする関数です。algorithmヘッダーにあります。
詳しくは:
for_each - cpprefjp C++日本語リファレンス
std::vector<int> vec3(100000, 1); std::for_each(vec3.begin(), vec3.end(), [](int& x) {x *= 2;});
結果
time[msec] | |
---|---|
①for | 2.2172 |
②range-based for | 4.9361 |
③for_each | 4.7552 |
forを使ったほうが2倍以上速いという結果になった。
C++ 配列コピーの速度
パディング処理をして新しい配列を作るときに、どのようにしてコピーするのが速いのか気になったので、比較してみた。
下図のような処理を、①逐次コピー ②std::copy ③memmove ④memcpyで実装して速度を比較してみた。
元の配列の大きさをcol × row、パディングをpadとした。
①逐次コピー
std::array<int, (row+2*pad)*(col+2*pad)> arr1 = {}; for (int i = 0; i < col; ++i) { for (int j = 0; j < row; ++j) { arr1[(i+pad)*(row+2*pad) + (j+pad)] = arr[i*row + j]; } }
②std::copy
std::array<int, (row+2*pad)*(col+2*pad)> arr2 = {}; for (int i = 0; i < col; ++i) { std::copy(arr.begin() + i*row, arr.begin() + (i+1)*row, arr2.begin() + (i+pad)*(row+2*pad) + pad); }
③memmove
std::array<int, (row+2*pad)*(col+2*pad)> arr3 = {}; for (int i = 0; i < col; ++i) { std::memmove(&(arr3[(i+pad)*(row+2*pad) + pad]), &(arr[i*row]), row*sizeof(int)); }
④memcpy
std::array<int, (row+2*pad)*(col+2*pad)> arr4 = {}; for (int i = 0; i < col; ++i) { std::memcpy(&(arr4[(i+pad)*(row+2*pad) + pad]), &(arr[i*row]), row*sizeof(int)); }
結果
col * row = 300 * 300, pad = 1として実験を行った。 chronoクラスのsteady_clockで実行時間を測定した。
それぞれ10回づつ実行した時の平均値はこのようになった。
time[μsec] | |
---|---|
①逐次コピー | 924.4 |
②std::copy | 40.1 |
③memmove | 21.9 |
④memcpy | 19.9 |
memcpy vs memmove
memcpyはコピー元のbufferとコピー先にbufferが重なった時の動作が未定義という問題がある。 今回の処理では別のバッファに書き込んでいるので大丈夫だが、バッファが重なっていないことをcheckする処理を入れたほうが確実だろう。
ということで、バッファが重なっていないことをcheckする処理を入れてもmemcpyのほうが速いのか調べてみた。
if (!(arr4.begin() > arr.end() || arr.begin() > arr4.end()))
abort();
arrayで確保される領域は連続しているので、これでcheckできているはずである。
col * row = 300 * 300, pad = 1として実験を行った。 chronoクラスのsteady_clockで実行時間を測定した。
それぞれ10回づつ実行した時の平均値はこのようになった。
time[μsec] | |
---|---|
memmove | 25.5 |
memcpy | 25.0 |
300*300程度の配列では差はほとんどないことがわかった。
プログラムの進捗をTwitterで通知する
エポックごとに結果を出力するような時間のかかるプログラムをまわしていて、エポックごとの結果が出るたびになんらかの通知がくれば便利だなと思ったので作った。
作ったものの概要
結果が書き込まれるoutput.txtの行数を監視し、増えると最終行を読んでTwitterでReplyをとばすもの。 プログラムを回している間の暇な時間にぱぱっと作ったのでとても簡易的。
Replyをとばすプログラム
tweepyを使ってぱぱっと作りました。 twitterアカウントを複数持っていたので、アカウントの一つをbotとして他の自分のアカウントにReplyをとばすようにしました。
output.txtの行数を監視するプログラム
shellスクリプトでwcコマンドをぐるぐる回して監視しています。
最終的にはoutput.txtは30行になるはずなので、30行になったら監視を終了しています。
これで、observer.shと監視したいプログラムを実行すればokです。
CircleCI2.0+googletestでC++のテスト環境を構築
Docker Imageの作成
ubuntu:16.04のimageを元にしてclang+googletestの入ったImageを作成しました。
$ docker pull ubuntu:16.04 $ docker run --name my-ubuntu -it ubuntu:16.04 /bin/bash
clang+googletestの環境構築
# clangのinstall $ apt update $ apt install llvm build-essential clang # googletestのinstall $ apt install git cmake $ git clone https://github.com/google/googletest.git $ cd googletest $ cmake -DBUILD_SHARED_LIBS=ON . $ make $ cp -a googletest/include/gtest /usr/include $ cp -a googlemock/gtest/libgtest_main.so googlemock/gtest/libgtest.so /usr/lib/ $ exit
$ docker commit my-ubuntu wakanapo/clang-gtest $ docker push wakanapo/clang-gtest
Makefileの用意
CXX := clang++ CXXFLAGS := -std=c++14 -Wall -O2 -lgtest_main -lgtest SRCS := test.cc test: $(SRCS) $(CXX) $(CXXFLAGS) -o $@ $^ .PHONY: clean clean: rm ./test
yamlファイルの用意
yamlファイルは.circleciというレポジトリの下に置きます。
version: 2 jobs: build: docker: - image: wakanapo/clang-gtest steps: - checkout - run: name: Make test command: make test - run: name: Running test command: ./test
平成29年度NW午前Iを解いた
Word
- MTBF
平均故障間隔(Mean Time Between Failure)のこと。
稼働している時間を合計して故障回数で割ることで求められる。
MTBFが大きいほど、故障までの時間や稼働している時間が長く信頼性が高いと言える。
一回故障すると使えなくなるという場合(ハードディスク, SSDなど)はMTTF 平均故障時間が使われる。 - MTTR
平均修理時間(Mean Time To Repair)のこと。
修理している時間を合計して故障回数で割ることで求められる。
MTTRが小さいほど復旧や修理が早く保守性が高いといえる。 - プリエンション
マルチタスクのコンピュータ・システムが実行中のタスクを一時的に中断する動作であり、基本的にそのタスク自体の協力は不要で、後でそのタスクを再実行するという意味も含む。 - CSMA/CD方式
Carrier Sense Multiple Access with Collision Detectionの略
1. イーサネット上のノードは自身が通信を開始する前に伝送路に他のノードが通信を行っていないか確認する。
2. 伝送路に通信が流れていなければデータを送信する。
3. もし他のノードの通信があった場合は通信が終わるのを待ってからランダムな待機時間後に送信を行う。
4. イーサネットの伝送路につながっている全てのパソコンは、データが伝送路上に流れてくると自分宛のデータかどうかチェックして、自分宛のデータだけを取り込み、それ以外は捨てる。
5. 複数のノードが同時に通信を行ってしまった場合には、コリジョンが発生しそのデータは破棄される。コリジョンが発生するとケーブル内の電圧が下がりケーブルに沿って電気信号が戻ってくる。これをジャム信号と呼ぶ。
6. イーサネット上の各ノードがどのジャム信号を検知したらデータを送信するのをやめる。伝送路が開くのを待ってからランダムな待機時間後に再度送信する、再送の回数は最大で16回と決められており、16回で通信に成功しないと、通信失敗としてOS側へ通知される。 - ドライブバイダウンロード攻撃
Webにアクセスした際などに、その本人の知らないうちに何らかのソフトウェアを勝手にダウンロード、インストールする手口のこと。 - サイバーレスキュー隊
被害の低減:標的型攻撃メールが届いている組織や、検知した不審通信や 不正ログなどに対してその深刻度を認識できずにいる組織に 対して、標的型攻撃メールや組織のログ等の情報を分析する ことにより、感染経路の把握、感染の範囲などを分析し、必 要な対策の早期着手を支援します。発覚した攻撃のフェーズ に沿った支援活動のイメージを図 2.3 に示します。
被害の拡大防止: 標的型サイバー攻撃の事案の対応の中で、標的型サイバー攻 撃による感染の連鎖(図 2.2 参照)を解明し、一連の攻撃の対 象となっていることを検知できずに「潜伏被害」を許してし まっていた場合に、その組織にコンタクトすることにより、 攻撃の連鎖の遮断を支援します - WAF
Web Application Fireallの略。外部ネットワークからの不正アクセスを防ぐためのソフトウェアであるFireallの中でも、Webアプリケーションのやり取りを把握・管理することによって不正侵入を防御ですることのできるFireallのこと。 - CMMI
Capability Maturity Model Integration(能力成熟度モデル統合)の略。組織がプロセスをより適切に管理できるように成ることを目的として順守すべき指針を体系化したものである。 - EVM
Earned Value Managementの略。プロジェクトの計画予算と実際に発生した費用、およびそれまでに完了した作業量を対比することで、コストおよびスケジュール実績が計画とどの程度の乖離があるのかを明確化し、最終的な推定コスト・完了時期を予測する。 - To-Beモデル
企業などのあるべき姿や将来的な理想の姿を表す。 - EA参照モデル
政府が公開しているEAの雛形のこと - ザックマンモデル
1987年ジョン・A・ザックマンによって提唱されたEAの基礎となっているフレームワークで、5種類の異なる立場の視点とそれぞれの5W1H(What、How、Where、Who、When、Why)の側面から分析・記述する手法。 - データモデル
対象業務を抽象的な形式で、情報システムやDBMSにおけるデータの表現方法としてモデル化したもの。 - デルファイ法
専門家グループなどが持つ直観的意見や経験的判断を反復型アンケートを使って、組織的に集約・洗練する意見収束技法。
Knowledge
- JIS X 8341:2010(高齢者・障害者等配慮設計指針ー情報通信における機器、ソフトウェア及びサービスー第一部共通指針)
多様な人々に対して、利用の状況を理解しながら、多くの個人のアクセシビリティ水準を改善できるようにすることを目的としている。 - 重複データを除外して取得
SELECT DISTINCT ~ - 評価指標
(最終成果物に含まれる誤りの件数)÷(最終成果物の量)=(信頼性の評価指標)
(修正時間の合計)÷(修正件数)=(保守性の評価指標)
(変更が必要となるソースコードの行数)÷(移植するソースコードの行数)=(移植性の評価指標)
(利用者からの改良要求件数)÷(出荷後の経過月数)=(機能性の評価指標)
Link
http://www.pc-master.jp/words/mtbf-mttr.html
https://www.itbook.info/study/p26.html
https://japan.norton.com/drive-by-download-7958
https://www.ipa.go.jp/files/000047187.pdf
http://sm.seeeko.com/archives/65793782.html
http://www.ap-siken.com/kakomon/
http://www.itmedia.co.jp/im/articles/0805/26/news130.html
Deep Learning Day 2018に参加してきた
1月20日土曜日にDeep Learning Day 2018に参加してきました。
私が発表した内容については前の記事に書いたとおりです。
Deep Learning Day 2018とは
「先端人工知能II」という授業の最終成果発表会です。この講義は、公開講義になっていて社会人の人も「Deep Learning応用講座」という名前で受講することができます。ただし、対象は「先端人工知能論I(Deep Learning 基礎講座)」を受講済みの人なので注意してください。
一授業の最終成果発表会とはいえ、秋葉原UDXの会場を借りて、Deep MindやFacebook AIの人が基調講演に来てくださり、昼食・夕食も出る強いイベントです。"金"って感じでした。
発表
すごく面白いものを作っているチームがたくさんあったので私の印象に残ったものをいくつか紹介したいと思います。
①Deep Quoridor
このチームはコリドールというゲームのAIを作っていました。ボードゲーマ(ガチではない)の私は、もともとこのゲームをしっていたので中間発表で話を聞いた時から面白そう!と思っていました。ただ、コリドールのAIは調べると既に作っている人がいた(Quoridor AI)のでそのへんどうなのだろうと思っていました。
実際にDeep Quoridorのデモを試してみると、Deep Quoridorのほうが既存のものよりもレスポンスが早いと感じました。また、UIも綺麗だと思います。どちらのほうがAIとして賢いのかはわかりません・・・・。どちらのAIにも私は勝てないのでwww気が向いたら、この2つのAIを直接対決させてみたいと思います。
②画風変換を駆使した顔はめパネル
漫画の主人公とかの美少女の顔はめパネルとかあると思いますが、せっかく顔はめして体と雰囲気は美少女になったのに、顔だけ元のままだと浮いちゃうし悲しい!みたいなのを改善してくれるものです(?)
顔の部分をパネルの画風に合わせて良い感じにしてくれます。
また、背景も絵風に変換してくれるので、実世界で顔はめしてとった写真から実際の漫画やアニメのワンシーンみたいな画像が生成できます。
③ANIBODY
人の動きに合わせてアバターをうごかせるようにしたものです。
発表の際レイテンシに気をつけたと強調していましたが、アイドルが複数人で踊っているような動画に対しても、ほぼレイテンシなく人数分のアバターを踊らせることができてました。
④俳句ジェネレーター
画像を与えると、それに合うような俳句を出力してくれるというものです。
正直言って、それほど精度はよくなかったです。575になっていない、季語が入っていない、日本語として意味がとれないというものも多々生成されてしまっていました。
ですが、私がためした卵かけご飯の写真ですごく良い感じなのが生成されて作成者の方もたまたまだと言っていて面白かったです。
⑤子供ジェネレータ
男女の顔写真を与えると、推定される子供の顔写真を生成してくれるものです。
テーマを聞いた時は、なんだよくあるやつか。と思っていたんですが、結構クオリティが高くて面白かったです。
会場にいた人はほぼ男性だったので、会場ではガッキーの写真と合成するデモをやっていました。
試してみますか?といわれて、思わず「えぇぇぇぇ、私とガッキーを合成するんですか・・・・?」となってしまいました。
だって、単純計算で2 * (子供の顔面偏差値) - (ガッキーの顔面偏差値) = (私の顔面偏差値)じゃないですか・・・(顔面偏差値は線形ではないのでは?とかそういう議論はいらない)。もし生成された子供が可愛くなかったら、完全に私のせいじゃないですか・・・。
冷静に考えると、別にそれは私が男であってもそういうことだし、それはそうって感じなんですが、別にガッキーとの合成じゃなくてもデモできるとのことだったので、デモしてくださってた方と私の子供の顔を生成しました(笑)
会場がちょっと暗めだったのか、生成された画像も暗っぽくなってしまいましたが、わかる〜って感じの画像生成されました。生成される子供の年齢をいじることもできます。
デモはJupyter Notebookでローカルで行っていたので、「こっそりいろんな人と試したいからぜひWebで公開して!」とお願いしておきました。
⑥FindVoice
ある人物の声のデータを約10分分与えると、それを学習し、動画などからその人物の発話シーンを検出することができるようになります。精度も非常に高くて素晴らしかったです。
Data Augmentationを駆使しているため、与えるデータ量も10分程度でいいという点が非常に実用的だなと思いました。
おわりに
面白そうだなと思った方はおそらく来年度も開講されると思うのでぜひ受講してみるといいと思います!
※アプリケーションの概要と公開されているリンクしか載せないようにしましたが、載せないで欲しいとか間違っているとかあれば編集するので教えてください。