gotutiyan’s blog

プログラミング関連の話題を中心に

Processingで「時間」を扱う一般的なテクニック

はじめに

本記事では時間を扱うテクニック全般を紹介します.

時間を扱いたくなる場面としては,ゲーム制作であれば,ステージをクリアするのに時間制限を設けるとき,必殺技的な何かを打った時のクールダウンを設定するときなどが考えられます.アート作品のポートフォリオでは,定数時間で他の作品に移るような処理が考えられます.もしくは学校の課題では,アナログ時計などを作るかもしれません.

このような時間全般の話題を網羅するのが本記事の目指すところです.それぞれの内容は独立しているので,必要そうなところだけ読んでいただければと思います.

目次

時間を扱うためのメソッド・システム変数

まずは,時間を扱うためのメソッドやシステム変数を紹介します.

hour(), minute(), second()

まさに時間を返すメソッドです.順番に時,分,秒を表していて,リアルタイムと連動しています.

void setup(){
  size(500,500);
  textSize(50);
  fill(0);
}

void draw() {
  background(255);
  int s = second(); 
  int m = minute();  
  int h = hour();    
  text(h+":"+m+":"+s,50,100);
}

millis()

実行時からのミリ秒を表します.1秒=1000ミリ秒です.second()との違いはミリ秒であることと,何よりも実行時からの時間を表すことです.

プログラムが実行された瞬間を0秒として,時間が増えていきます.1000で割り算をすれば秒だけ抽出,1000.0で割り算をすれば秒に加えて小数点以下が表示されて,時間経ってる感が出ます.

void setup(){
  size(500,500);
  textSize(50);
  fill(0);
}

void draw() {
  background(255);
  text(millis(),50,100);
  text(millis()/1000.0,50,200);
  text(millis()/1000,50,300);
}

frameCount

実行時からのフレーム数を表すシステム変数です.これは時間そのものではないですが,時間と共に増加しますし,値の増え方がsecond()millis()の中間くらいなので場面によっては使いやすいです.

void setup(){
  size(500,500);
  textSize(50);
  fill(0);
}

void draw() {
  background(255);
  text(frameCount,50,100);
}

時間を0から計測しなおすテクニック

(本記事なんですが,これを書きたかったので書いているまであります.)

ある時間制限を設けているようなゲームで,ゲームオーバーになったとします.ゲームはリスタートできて,プレイヤーは何度も挑戦することになります.この時,時間制限は毎回0から数えるべきです.

時間計測系の処理は,基本的にmillis()を用いるのが簡単です.しかし,millis()は実行時から増える一方なので,「途中でまた0から始めたいとき」は,millis()だけでは対応できません.ここではその解決方法について紹介します.

解決方法といっても,複雑なものではありません.以下の構造で実現可能です.

int base_time = 0;
void setup(){
  size(500,500);
}

void draw() {
  int time = millis() - base_time;
  
  if(/*0から数え直したい条件*/){
    base_time = millis();
  }
}

ある変数を用いて,millis()-変数の形にするのがミソです.上記では変数名をbase_timeとしています.

実行した瞬間は,
millis()-変数 = millis()-0 = millis()
であるため,millis()そのものを表します.そして,0から数え直したい条件を満たせば変数にその時点でのmillis()を代入することで,その瞬間の式の値は
millis()-変数 = millis()-millis() = 0
となります.その後,また第1項目のmillis()は増加し続けるので,millis()-変数はまた増えていきます.

以下のサンプルは,画面をクリックすれば時間が0から数え直されるコードです.先ほどのコードでは,if(/*0から始めたいきっかけ*/)の中でbase_timeの更新処理をしていましたが,必ずしもif文の中で行うとは限らず,mouseClicked()のような関数の中でやる場合もあります.

int base_time = 0;
void setup(){
  size(500,500);
  textSize(50);
  fill(0);
}

void draw() {
  background(255);
  int time = millis() - base_time;
  text(time/1000.0, 50, 100);
  
}

void mouseClicked(){
  base_time = millis();
}

周期性:時間から角度へ

ここでは周期性と時間について扱います.様々な場面で使用する頻出のテクニックです.「周期性」というのは,ずっと見ているといつか同じ状態に戻ってくるようなものです.時計は一日中眺めていると同じ時間に戻ります.

周期的なものを表すには,「時間を角度とみなして三角関数の中に入れる」ということをします.例えばアナログ時計では,時間を長針の角度,分を短針の角度に変換していると考えられます.このような実装について具体例とともに見ていきます.

アナログ時計の短針の実装を例に 〜map()メソッド〜

時間から角度というように,ある単位の値を他の単位に変換したいときは良くあります.アナログ時計であれば,0~60という「分」である単位を,0~360度,もしくは0~2πラジアンという「角度」に変換したいということです.このような時に役立つのがmap()メソッドです.

map(変数, 元の単位の下限,元の単位の上限,新しい単位の下限,新しい単位の上限)
// 0~59を取る秒という単位を,0~2πという角度の単位に変換
rad = map(second(), 0, 59, 0, 2*PI)

このメソッドは,下限の上限の範囲の同じ割合のところに値を変換します.例えばsecond()の値が30なら,0~59の範囲では真ん中なので,変換後は0~2πの範囲での真ん中,つまりπに変換されます.

map()は変換後の数値を返すため,戻り値をそのままsin()やcos()の引数に入れることができます.以下の例では,1秒ごとに線が動いていきます.これはアナログ時計における短針の実装そのものです.

void setup(){
  size(500,500);
}

void draw() {
  background(255);
  // 秒を角度に変換
  float rad = map(second(), 0, 59, 0, 2*PI);
  // それを三角関数の引数に
  float x = width/2 + 100*cos(rad);
  float y = height/2 + 100*sin(rad);
  line(width/2, height/2, x, y);
}

f:id:gotutiyan:20200805113702p:plain
実行例

millis()やframeCountを角度に変換するには?

先ほど短針の実装をしましたが,ここでfloat rad = map(second(), 0, 59, 0, 2*PI);という変換をしていました.ただこれは,second()が0~59しか取らないことが分かっているから書けることです.millis()やframeCountは無限に増え続けるため,上限がありません.つまりmap()の第3引数の値が書けないんですね.

でも,あまり問題はありません.というのは,結局三角関数に入れてしまうからです.sinxのグラフを書けばわかるように,三角関数というのは,引数の値がどれだけ大きくなっても,1から-1の間をウロウロする関数です.なので,そもそもmap()は使わずに,millis()やframeCountを直接sin()の中に入れてしまえば良いです.これでも,周期性は実現できます.

f:id:gotutiyan:20200807191050p:plain
sin(x)のグラフ

ただし,「ちょうど1200ミリ秒で周期的にさせたい!」とか「ちょうど45フレームで周期的にさせたい!」というように,周期をすごく厳密に定める場合には,余りをとって,0~2πに変換する方法が考えられます.ある整数modで余りをとった時,その結果は0~mod-1になるので,map()を用いてこの範囲を0~2πの範囲に変換すれば良いです.先ほどの短針の実装も,second()に対してmod=60で余りを取っていると考えると同じパターンです.

void setup(){
  size(500,500);
}

void draw() {
  background(255);
  // ちょうど50フレーム周期にしたい!
  int mod = 50;
  // modの余りをみて,0~mod-1の範囲を0~2*PIの範囲へ変換
  float rad = map(frameCount%mod, 0, mod-1, 0, 2*PI);
  // それを三角関数の引数に
  float x = width/2 + 100*cos(rad);
  float y = height/2 + 100*sin(rad);
  line(width/2, height/2, x, y);
}

時間変数を自分好みの速さに調整する

時間変数の「増える速さ」に注目すると,hour()<minute()<second()<frameCount<millis()というような優劣がついています.時間をどんな処理に使うかにもよりますが,大体は思い通りの早さでは無いため,そのまま使える場面は少ないです.そんな時は,適当に掛け算や割り算をすることで調整します.

以下はframeCountを使う場合のサンプルです.

void setup(){
  size(500,100);
}

void draw() {
  background(255);
  ellipse(frameCount,50,30,30);
  ellipse(frameCount/2,50,30,30);
  ellipse(frameCount/4,50,30,30);
}

割る数が2倍になれば,当然速さは1/2になります.この一つ前で書いた周期性と関連づけるのであれば,周期が1周するのにかかる時間が2倍になるとも言えます.このように適当に割ったり掛けたりすることで,「目的の値の増え方」を得るというのは必須テクニックです.

おわりに

ここでは時間を扱う一般的なテクニックを紹介しました.他にもこんな処理の解説が欲しいということがあれば,コメントに書いていただけると追記するかもしれません.

またこの他にも,シーン遷移に関するテクニックを紹介した記事もあるのでよろしければご覧ください.

gotutiyan.hatenablog.com

甲南大学知能情報学部の成績優秀者表彰制度について

はじめに

本記事は,表題の表彰制度の報酬やGPAのボーダーの予想について書きます.筆者は2年連続これを受賞しています(自慢です,というのは嘘で,記事の信ぴょう性のためです.).

この表彰制度について

簡単に言うと,良い成績(=高いGPA)を取れば受賞できるものです.(あくまでも予想ですが,可能性は極めて高いです.)

この表彰制度は,以下のように2回の受賞チャンスがあります.

  1. 2回生後期までの成績での受賞
  2. 3回生後期までの成績での受賞

2回連続で受賞することも可能です.

受賞者の決定については,単純にGPAの高い順に取っていく感じだと思います.(選定方法は非公開なので,正確なことはわかりません.)

また,受賞人数にも差があります.2回後期の枠では10人超えるくらいが受賞します.おそらく上位10%と行ったところです. 対して,3回後期の枠では5人くらいが受賞します.こちらはおそらく上位5%といったところです.

3回後期の枠の方が人数が少ないため,2回後期の時点で受賞できなければ,3回後期で受賞するのは非常に困難だと思います.

報酬について

報酬は,表彰状と図書カードがもらえます.嬉しいですね.

さらに,2回後期の優秀者のうち,さらに上位の人は,特待生の制度によって60万円くらいもらえます(上位2人くらい?).なんかこれより上の金額もあるとか無いとか.

受賞に必要なGPAについて

これに関しても,正確なことは分かりません.

筆者は2年連続受賞しているので,参考資料として載せておきます.

2年後期では,平均GPA3.51でした.
3年後期では,平均GPA3.59でした.
(もちろん,選考基準として平均GPAを見られているかどうかすら分かりません.)

この成績では60万円はもらえなかったので,高みを目指すのであれば少なくとも平均GPA3.6は必要だと思います.

おわりに

ここでは表題の表彰制度の詳細を説明しました.
ぜひ受賞を目指してください.

bibを入力とした論文読みサイトを作ってみた

はじめに

論文管理,大変ですよね.日本語や英語問わず,論文を読むようになって思うのは,「論文へのアクセスに時間かかるな」ということです.一度読んだ論文をもう一度読みたい時.

  1. タイトルや著者もしくは会議で検索

  2. 学会の論文ページに飛ぶ

  3. 論文のPDFを開く

というような工程が必要です.特に検索フェーズが煩わしいです.人類が論文管理ツールを求める気持ちが良くわかりました.

このような背景から,論文の一覧がざっと見れて,簡単に検索できて,その論文のPDFにすぐに飛べるようなものを作りたいと思いました.また,個人的な話ですが,読んだ論文情報は自分の論文を書く時に引用する可能性も高いので,とりあえずbibに突っ込むことが多いです.よって,bibをうまく使いたいと思いました.そこで,以下のようなサイトを作りました.

gotutiyan.github.io

リポジトリはこちら:
https://github.com/gotutiyan/reading_paper_counfortable

公開にはGitHub Pagesを利用していますので,URLをブックマークしたり,リポジトリをクローンしてローカルで動かす事もできます.

bibの構造

bibは,論文の情報を表したものです.

@inproceedings{vaswani2017attention,
  title={Attention is all you need},
  author={Vaswani, Ashish and Shazeer, Noam and Parmar, Niki and Uszkoreit, Jakob and Jones, Llion and Gomez, Aidan N and Kaiser, {\L}ukasz and Polosukhin, Illia},
  booktitle={Advances in neural information processing systems},
  pages={5998--6008},
  url={https://arxiv.org/abs/1706.03762}
  year={2017}
}

上記のように,タイトルとか著者とか,会議名,年度,urlなどを指定できます.Texで論文を書く時には,このような形式で論文情報を管理することが一般的です.urlの情報が入っていることが大きくて,この情報を利用することで便利なサイトが作れそうだと思いました.

ざっくりとした機能

今回作ったwebサイトの機能を簡単に紹介します.論文の情報は表形式で閲覧できて,1行が1論文に対応しています.一番右のLINKは論文ページのリンクです.

f:id:gotutiyan:20200728123017p:plain
論文の表示形式

各項目は検索もできます.正規表現も利用できて,複数の欄に入力した場合,AND検索になります.

f:id:gotutiyan:20200728124838p:plain
検索欄

支える技術

言語としてはHTML,CSSjavascript,ライブラリはbootstrapです.

全体的な処理

ユーザは入力するbibファイルを選択します.その後,javascriptのFileReaderを用いて中身を一旦抽出します.この時,正規表現を使って各情報を抽出し,構造体のようにしてまとめたオブジェクトを配列に入れていく感じです.

次に,配列の中で,検索で指定された条件に合わないものを除外します.

最後に,配列に残った情報をもとに,表を構築します.論文の情報はtable要素で表示しているため,th要素やtr要素を動的に生成して追加していくような処理になります.

おわりに

こういうのを発展させていくと,ログイン機能をつけて,各ユーザーの論文情報をサーバーに保存して... みたいなことになるんでしょうね.現状そこまでするつもりはありませんが...

もし良ければ,使ってみてください^^

【入門】processingで最小限のブロック崩しを作る

この記事は,processingを用いてブロック崩しを作るためのチュートリアルです.初心者でも完成までたどり着けるように、詳しく書いていきたいと思います. 出てくるプログラムの知識は int型, float型、変数、if、for程度です.どれも事前知識があると嬉しいですが、全て解説は入れているので、問題ないのではないかなと思います.

完成図は以下のようになります.

f:id:gotutiyan:20200610130202g:plain
完成図

ではでは.

はじめに

座標系

processingを使うと、自由に図形を描くことができます。図形を描画する際に必ず指定するのが座標なのですが、processingの場合は座標系における軸の取り方がいつもとは違います。 数学における座標系は、上に行くほどy座標が増えて、右に行くほどx座標が増えます。また、原点は左下です。 対して、processingにおける座標系は、下に行くほどy座標が増えて、右に行くほどx座標が増えます。また、原点は左上です。

f:id:gotutiyan:20200102190010p:plain
座標系の違い(左:数学 右:processing)
最初は戸惑うかもしれませんが、徐々に慣れていきましょう。

setup()とdraw()

ゲームを作るために欠かせないのは「パラパラ漫画」の機構です。ページを素早くめくるように、ゲームは高速に画面が更新されています。例えばテトリスではブロックが落ちていきますが,落ちていくように見えるのは画面が素早く更新されているからです.この更新の速さを「フレームレート」と呼んだりしますが、フレームレートが低いことは、いわゆる「カクつく」「処理が重い」と感じる原因です。パラパラ漫画をめくる速度が遅ければ、当然絵もカクつきますね。

さて、processingにはこの「高速に画面を更新する」仕組みがあります。それがsetup()draw()です。これらはプログラミングの中では非常に重要な「関数」というものですが、今は割愛します。 setup()は、ゲームが始まった瞬間に一度だけ処理が行われます。 draw()は、ゲームを遊んでいる間、裏で何回も何回もひたすら処理が行われます。draw()が何回も実行されて、画面がその度に更新されることによって、パラパラ漫画はめくられます.ブロックは落ちるし、キャラは動きます。

具体的には、以下のようなテンプレを元に作成することになります。

void setup(){
  //1回だけ実行される処理
}

void draw(){
  //何回も実行される処理
}

初期設定

今回ゲームを作るにあたって、初期設定をしましょう。 まずは実行画面の大きさです。ゲームをプレイする画面を小さくしたり、大きくしたりできます。 これはsize()によって指定することができます。サイズは一度設定すればそれ以降固定されるので、setup()に書きます。 サイズは何でも良いですが、本記事では500*500にすることにします。

void setup(){
  size(500, 500);
}

void draw(){
  //何回も実行される処理
}

実行ボタンを押すと、ある程度の大きさのウィンドウが表示されます。

ボールを作る

まずはボールを動かしましょう.ブロック崩しにおけるボールは,プレイヤーの意思に関わらず動き続けます.それに,壁やブロックに当たれば跳ね返ります.まずはブロックもバーも無いものと仮定して,壁に跳ね返るところまで実装します.

ボールの描画

ボールを画面に表示させます.まずは,画面の中心に円を描くことからやってみます.円はcircle(中心のx座標, 中心のy座標, 直径)で描けます.円はellipse()と覚えているかもしれませんが,最近のバージョンでcircle()も追加されました.

void setup(){
  size(500,500);
}

void draw(){
  background(255); //背景を白に
  circle(250, 250, 100); //円
}

実行すると,画面に円が表示されます.

ついでに,早めにbackground(R, G, B)も紹介しておきます.これは背景の色を指定するものです.R,G,Bの3つの要素で色を指定します.例えば,(0,0,0)なら黒,(255,255,255)なら白,(255,0,0)なら赤になります.そしてもう一つ便利な機能があって,RにもGにもBにも同じ値を指定するときに限っては,1つだけ指定することで同じことができます.つまり,background(255,255,255) = background(255)です.

ボールの話に戻りますが,今の状態では,円を"動かす"ことはできません.なぜかというと,円の座標を250という定数で指定しているからです.例えば,この値が250→251→252→253→...と変化していけば,円は動いているように見えるはずです.そのために,変数を使うことにします.

変数は,ある特定の値を保存できるものです.250のような「定数」と比べて「変数」が便利なのは,保存した値に対して足し引きなどの演算ができることです.また,変数にはがあります.これは保存できる値の種類を示すもので,本記事では以下の2つをよく使います.

  • int型:整数を保存できる型....-2 -1 0 1 2 ...
  • float型:小数を保存できる型.3.14 2.0 1.999など
  • boolean型:真偽値を保存できる型.truefalseだけ.

では,円の座標を変数で置き換えてみます.

// 変数の宣言
float ball_x, ball_y, ball_r;

void setup(){
  size(500,500);
  // 初期値の設定
  ball_x = 250;
  ball_y = 250;
  ball_r = 100;
}

void draw(){
  background(255);
  // 変数に置き換えた
  circle(ball_x, ball_y, ball_r); 
}

float型の変数を3つ作成して,circle()の中身を置き換えました.変数は必ず「宣言」してから使います.宣言は型名 変数名で行います.変数名は自由に決めることができるので,ball_x以外の名前でも構いません.宣言したら,実際に値を代入できます.初期値の代入は最初の一回だけ行えば良いので,setup()に書きます.

実行結果は何も変わっていませんが,変数で置き換えることには成功しました.

ボールを動かす

では,ボールを動かしていきます.動かすためには,速さの定義が必要です.つまり,ball_xball_yがどれくらいの速さで動くかということです.速さも変数にしましょう.

float ball_x, ball_y, ball_r;
// 速度の変数を宣言
float speed_x, speed_y;

void setup(){
  size(500,500);
  ball_x = 250;
  ball_y = 250;
  ball_r = 100;
  // 速さの初期値を設定
  speed_x = 1;
  speed_y = -2;
}

void draw(){
  background(255);
  circle(ball_x, ball_y, ball_r);
  // 座標を更新
  ball_x += speed_x;
  ball_y += speed_y;
}

速さを表す変数を,speed_x, speed_yという変数名で宣言しました.それぞれx座標の速さと,y座標の速さを表しています.これは,座標の変数の値に対して,速さの変数の値を足します.例えば,ball_xは初期値250で,speed_x1なので,ball_x += speed_xはフレームを重ねるたびに,250→251→252...と増えていきます.同じように,ball_y250→248→246...と減っていきます.ボールの座標の更新は毎フレーム行うべきなのでdraw()に書きます.

実行すると,円が動き出しました!

円を跳ね返す(進む向きを反転させる)

円が動いたのは良いですが,見ているとすぐに画面外に消えてしまいます.これではゲームにならないので,壁で跳ね返るようにします.

まず,「跳ね返る」ことについて考えます.実は跳ね返りは,x軸方向とy軸方向で分けて考えます.例えば右の壁に当たって跳ね返ることを考えると,x軸方向の向きは反対になりますが,y軸方向の向きは変化しないのです.ベクトルを分解するイメージがある人はわかりやすいと思います.

f:id:gotutiyan:20200609100243p:plain
跳ね返りの様子.

同様に考えると,左右の壁に当たったときはx軸方向の向きだけが反転し,上下の壁に当たったときはy軸方向の向きがだけ反転します.
また,「向きが反転する」というのは,正の方向に進んでいたものが負の方向になる,もしくはその逆です.つまり,速度に-1をかけるだけで実装できます.

// 進む向きを反転させる例
speed_x *= -1;
speed_y *= -1;

円を跳ね返す(当たり判定)

速度に-1をかけるのは,あくまでもボールが壁に当たったときにやることです.そもそも「壁に当たった」ことを検知できないといけません.
そのために,当たり判定を実装していきます.基本的に,if文で実装します.

if文は,特定の条件を指定できるものです.「ある変数の値がこの値の時」とか「この変数の値がこの変数の値より大きい時」みたいなことが書けます.また,これらの条件を複数指定して,全て同時に満たさないといけないとか,いずれか1つ満たせば良いなども指定できます.

// ifの使用例
if(a == b){ 処理 } // aとbが等しい
if(a > b){処理} // bよりaの方が大きい
if(a == b && c == d) // aとbが等しく,cとdも等しい(同時に満たす必要がある)
if(a == b || c == d) // aとbが等しい,もしくはcとdが等しい(どちらか一方満たせばok)

さて,ボールを跳ね返す時はどういう時かを考えると,ボールが壁にめり込む直前(直後?)であることが分かります.これをどうif文に落とし込むかが大事です.

まず,ボールの上下左右の点の座標を考えます.

f:id:gotutiyan:20200609121257p:plain
ボール4点の座標

ちょっと分かりにくいかもしれませんが,こうなります.ball_rは直径なので,/2します.この座標と,画面の端の座標との大小を見ることによって,壁にめり込むかどうかを判定できます.

float ball_x, ball_y, ball_r;
// 速度の変数を宣言
float speed_x, speed_y;

void setup(){
  size(500,500);
  ball_x = 250;
  ball_y = 250;
  ball_r = 100;
  // 速度の初期値を設定
  speed_x = 1;
  speed_y = -2;
}

void draw(){
  background(255);
  circle(ball_x, ball_y, ball_r);
  ball_x += speed_x;
  ball_y += speed_y;
  
  // 当たり判定!
  if(ball_x + ball_r/2 > width) speed_x *= -1; // 右の壁に当たる時
  if(ball_x - ball_r/2 < 0) speed_x *= -1;  // 左の壁に当たる時
  if(ball_y - ball_r/2 < 0) speed_y *= -1; // 上の壁に当たる時
}

widthというのは画面の幅を表す値で,システム変数と呼ばれます.システム変数は,processingが勝手に値を入れてくれます.具体的には,冒頭でsize(500, 500)と指定した時に,勝手にwidthには500が保存されています.蛇足ですが,同様に画面の高さを表すheightというシステム変数にも500が保存されています.

バーを作る

ボールを跳ね返すためのバーを作ります.これも,当たり判定を考慮しないといけません.

一つ前に紹介した,ボールが壁で跳ね返るというのは,「円と線の当たり判定」だったので簡単でした.ですが,バーを使って跳ね返すというのは,「円と四角の当たり判定」です.これは少々めんどくさいのです.でも,やります.

ちなみに,バーは四角でなくてただの線でも十分実装できます.でも,後でブロックを実装した時に結局円と四角の当たり判定は必要になるので,敢えてバーを四角にして先取りすることにします.

マウスに追従させる

まずはバーを描いて,マウスに追従させます.まずは四角の描画からです.四角はrect(左上のx座標,左上のy座標,幅,高さ)で書けます.

また,新しいシステム変数としてmouseXmouseYを紹介します.これはマウス座標を表します.これを直接バーの座標とすることで追従するようになります.

// ここではdrawと宣言だけ書いている.
float bar_w = 100, bar_h = 30;
void draw(){
  background(255);
  circle(ball_x, ball_y, ball_r);
  ball_x += speed_x;
  ball_y += speed_y;
  
  if(ball_x+ball_r/2 > width) speed_x *= -1;
  if(ball_x-ball_r/2 < 0) speed_x *= -1;
  if(ball_y - ball_r/2 <0) speed_y *= -1;
  // バーの描画
  rect(mouseX, mouseY, bar_w, bar_h);
}

円と四角の当たり判定

さて,ここが少し難しいですが,とても重要です.現在はバーに注目していますが,ブロックを作る時にも同じ考え方をします.まずは,バー座標がどのように表されるのかを以下に示します.ボールの座標についても,先ほど載せたものを再掲します.

f:id:gotutiyan:20200609175522p:plain
バーの頂点の座標.

f:id:gotutiyan:20200609121257p:plain
ボール4点の座標

さて,これらの座標を使って当たり判定を実装します.ちなみに,本記事は「最小限」なので,上から降ってきたボールに対して当たることだけ考えます.本当は上下左右から当たるようにするべきですが,これでも十分成立します.

f:id:gotutiyan:20200609180331p:plain
この方向だけを考えるよ

この時,次の2点を満たす必要があります.

  • x座標で見たときに少しでもボールとバーが被っている.

  • y座標で見たときに,ボールの下の点がバーに被っている.

f:id:gotutiyan:20200609181024p:plain
x座標が被る例

f:id:gotutiyan:20200609181049p:plain
y座標が被る例

実装すると,こんな感じになります.正直,これは図形的なイメージを持ちながら,座標と真剣に向き合うしかありません.

float ball_x, ball_y, ball_r;
float speed_x, speed_y;
float bar_w = 100, bar_h = 30;

void setup(){
  size(500,500);
  ball_x = 250;
  ball_y = 250;
  ball_r = 50;
  speed_x = 1;
  speed_y = -2;
}

void draw(){
  background(255);
  circle(ball_x, ball_y, ball_r);
  ball_x += speed_x;
  ball_y += speed_y;
  
  if(ball_x+ball_r/2 > width) speed_x *= -1;
  if(ball_x-ball_r/2 < 0) speed_x *= -1;
  if(ball_y - ball_r/2 <0) speed_y *= -1;
  rect(mouseX, mouseY, bar_w, bar_h);
  // バーとボールの当たり判定(上からの分)
  if((ball_x + ball_r/2 > mouseX && ball_x - ball_r/2 < mouseX+bar_w)  //x座標に関するもの
    &&(mouseY < ball_y + ball_r/2 && ball_y + ball_r/2 < mouseY+bar_h)){ //y座標に関するもの
      speed_y *= -1;
    }
}

複雑な条件式ですが,使っている座標は先ほど画像で示したものだけしか使っていません.

これを実行すると,バーでボールを跳ね返せるようになることが分かります.

ブロックを作る

さて,ブロックを作ります.ここでは5*5の25個のブロックを作ることにします.要件としては

  • ボールと当たり判定が発生する

  • ボールと当たれば消える

の2点を目指します.

ブロックの描画

まずはブロックを描画します.ブロックは25個作ると決めているので,直感的にはrect()を25個書けば良いです.でもちょっとめんどくさいので,for文を使うことにします.

for文は繰り返しを行うものです.ざっくりした文法は
for(型 変数名 = 初期値; 変数名<限界値; 変化量の記述){処理}です.例えば,for(int i=0; i<5; i++)と書くと,iという変数が0 → 1 → 2 → 3 → 4で変化します.また,この時のiをループ変数と呼ぶことが多いです.iもただの変数名なので何でも良いですが,慣習的にi j kあたりがよく使われます.

また,ブロックの幅と高さはすぐにわかります.5*5なので,幅はいわゆるwidth/5 = 500/5で,100です.高さは適度に画面の上部に埋めたいので,適当に30にしましょう.これらは変数block_wblock_hで宣言することにします.

次は,横一列の5個だけ描画することを考えます.この時,5つのブロックのx座標は0, 100, 200, 300, 400です.y座標はいずれも0です.

f:id:gotutiyan:20200610152908p:plain
横一列のブロックとその座標

これを愚直に書けば

rect(0, 0, block_w, block_h);
rect(100, 0, block_w, block_h);
rect(200, 0, block_w, block_h);
rect(300, 0, block_w, block_h);
rect(400, 0, block_w, block_h);

となります.これを実行すると,横一列にブロックが並びます.でもちょっと冗長です.
ここで注目するべきなのが,x座標は100ずつ増えていることです.こういう等間隔に値が変化する時は,for文で書き直せることが多いです.

for(int i=0;i<5;i++){
  rect(i*block_w, 0, block_w, block_h);
}

これを実行しても,同じ結果が得られます.今の所,ソースコード全体としては以下のようになります.

float ball_x, ball_y, ball_r;
float speed_x, speed_y;
float bar_w = 100, bar_h = 30;
// ブロックの幅と高さを宣言
float block_w = 100, block_h = 30;

void setup(){
  size(500,500);
  ball_x = 250;
  ball_y = 250;
  ball_r = 50;
  speed_x = 1;
  speed_y = -2;
}

void draw(){
  background(255);
  circle(ball_x, ball_y, ball_r);
  ball_x += speed_x;
  ball_y += speed_y;
  
  if(ball_x+ball_r/2 > width) speed_x *= -1;
  if(ball_x-ball_r/2 < 0) speed_x *= -1;
  if(ball_y - ball_r/2 <0) speed_y *= -1;
  rect(mouseX, mouseY, bar_w, bar_h);
  
  if((ball_x + ball_r/2 > mouseX && ball_x - ball_r/2 < mouseX+bar_w) 
    &&(mouseY < ball_y + ball_r/2 && ball_y + ball_r/2 < mouseY+bar_h)){
      speed_y *= -1;
    }
    
  // まずは横一列
  for(int i=0;i<5;i++){
    rect(i*block_w, 0, block_w, block_h);
  }
}

では,これを25個に増やしましょう.いま,ブロックをfor文によって横に5個並べました.次はfor文で「横に5個並べたものを」縦に5個並べましょう.

f:id:gotutiyan:20200610153548p:plain

for(int j=0;j<5;j++){ //縦に5個並べる
  for(int i=0;i<5;i++){ //横に5個並べる
    //   ↓iを使う!  ↓jを使う!
    rect(i*block_w, j*block_h, block_w, block_h);
  }
}

ブロックが描画できました!

ブロックでも跳ね返るようにする

ブロックでもボールが跳ね返るようにします.円と四角の当たり判定は既にやっているので,ほとんど同じことをすれば良いです.唯一変わるのは,ボールの来る向きが違うので,下から来たボールに対する当たり判定になるということです.

実装としては,バーの当たり判定をコピぺして,バーの座標だったところをブロックの座標になるように書き換えます.また,ball_y + ball_r/2だったところを,ball_y - ball_r/2に書き換えます.注目するボールの頂点が下の頂点から上の頂点に変わったということです.

float ball_x, ball_y, ball_r;
float speed_x, speed_y;
float bar_w = 100, bar_h = 30;
// ブロックの幅と高さを宣言
float block_w = 100, block_h = 30;

void setup(){
  size(500,500);
  ball_x = 250;
  ball_y = 250;
  ball_r = 50;
  speed_x = 1;
  speed_y = -2;
}

void draw(){
  background(255);
  circle(ball_x, ball_y, ball_r);
  ball_x += speed_x;
  ball_y += speed_y;
  
  if(ball_x+ball_r/2 > width) speed_x *= -1;
  if(ball_x-ball_r/2 < 0) speed_x *= -1;
  if(ball_y - ball_r/2 <0) speed_y *= -1;
  rect(mouseX, mouseY, bar_w, bar_h);
  
  if((ball_x + ball_r/2 > mouseX && ball_x - ball_r/2 < mouseX+bar_w) 
    &&(mouseY < ball_y + ball_r/2 && ball_y + ball_r/2 < mouseY+bar_h)){
      speed_y *= -1;
    }
    
  for(int j=0;j<5;j++){
    for(int i=0;i<5;i++){
      rect(i*block_w, j*block_h, block_w, block_h);

      // 当たり判定! 
      if((ball_x + ball_r/2 > i*block_w && ball_x - ball_r/2 < (i+1)*block_w) 
        &&(j*block_h < ball_y - ball_r/2 && ball_y - ball_r/2 < (j+1)*block_h)){
          speed_y *= -1;
      }
    }
  }
}

複雑ですが,一度理解すればやっていることはそこまで難しくありません.

さて,これでやっと当たり判定が完成〜!と思いたいんですが,一つ問題があります.以下の図のような場合です.

f:id:gotutiyan:20200610005113p:plain
左:問題無し 右:問題あり

左のように,ボールのx座標がただ一つのブロックに被るなら問題ありません.普通に跳ね返ります.しかし,右の図のように,x座標が2つのブロックと被る場合,跳ね返りません.

この原因は,同じフレームで,AとBの2つのブロックに対して当たり判定が行われているからです.このとき,speed_y*=-1は2回実行されるので,結局元に戻ってしまいます.これを解決するため,魔法の1行を加えましょう.

if((ball_x + ball_r/2 > i*block_w && ball_x - ball_r/2 < (i+1)*block_w) 
      &&(j*block_h < ball_y - ball_r/2 && ball_y - ball_r/2 < (j+1)*block_h)){
        speed_y *= -1;
        ball_y = ball_r/2 + (j+1)*block_h+1; //これ!
}

これで,当たり判定の問題は無くなりました.あるブロックに当たった瞬間,そのブロックにギリギリ被らない位置にボールを移動させています.これにより,同一フレーム内では二度と当たり判定が起こらなくなります.

f:id:gotutiyan:20200610010013p:plain

ブロックが消えるようにする

これが最後の山場です!!!

ブロックが消えなければ,クリアできないブロック崩しになってしまいます.そこで,「ブロックが生きているか」という情報を変数で持つことを考えます.この情報は,25個のブロックそれぞれが持つことになります.ということで,配列を使って生きているかどうかを保存しましょう.

配列は,複数の変数を一つの変数かのように管理できるものです.例えば,何かしらのx座標を持つ変数を10個宣言したいとき,

float x1, x2, x3, x4, x5, x6, x7, x8, x9, x10;

と書くのは面倒です.配列を使えば

float x[] = new float[10];

だけで宣言できます.あくまでも変数はx一つだけしか宣言していませんが,x[0], x[1], ..., x[9]というように,[数字]を使って参照できます.この数字のことを添字indexと呼んだりします.添字は必ず0から始まります.

さらに,配列には次元があります.1次元から始まり,2次元,3次元といくらでも増やせます.
今回は,5*5の2次元配列で管理します.

また,変数の型はbooleanにします.booleanは冒頭でも紹介した通りtruefalseしか持たない型です.ここでは生きていればtrue,そうでなければfalseとします.

では実装です.まずは,boolean型の2次元配列を作ります.

boolean is_alive[][] = new boolean[5][5];

この配列はまだ値が入っていないので,setup()で初期値を入れたいですが,その前に,配列とfor文の相性に触れておきます.いま,配列の添字は0 1 2 3 4...と増えていきます.また,例えばfor(int i=0;i<10;i++)と書けばi = 0 1 2 3 4...と増えていきます.これらは全く同じ変化をしているので,for文の中でi(正確にはループ変数です)を直接添字にしてしまうことができます.配列とfor文は相性抜群なのです.

ではこの相性の良さを2次元配列で実感してみます.

// setup()の中の話
for(int i=0; i<5; i++){
  for(int j=0; j<5; j++){
    //最初は全部生きているので初期値はtrue
    // i と j を直接添字に
    is_alive[i][j] = true;  
  }
}

後は,if文にbooleanを用いた条件式を追加します.生きているかどうかで処理が変わるのは,ブロックを描画することと,当たり判定が発生することなので,2箇所書き加えることになります.

また,当たり判定が発生すればブロックは消えるべきです.消えるということは,trueだったものをfalseにするということです.falseになった途端,描画もされないし,当たり判定も発生しなくなります.

float ball_x, ball_y, ball_r;
float speed_x, speed_y;
float bar_w = 100, bar_h = 30;
float block_w = 100, block_h = 30;
// 「生きているか」を表すbooolean配列
boolean is_alive[][] = new boolean[5][5];

void setup(){
  size(500,500);
  ball_x = 250;
  ball_y = 250;
  ball_r = 50;
  speed_x = 1;
  speed_y = -2;
  // 全ての初期値をtrueに
  for(int i=0;i<5;i++){
    for(int j=0;j<5;j++){
      is_alive[i][j] = true;
    }
  }
}

void draw(){
  background(255);
  circle(ball_x, ball_y, ball_r);
  ball_x += speed_x;
  ball_y += speed_y;
  
  if(ball_x+ball_r/2 > width) speed_x *= -1;
  if(ball_x-ball_r/2 < 0) speed_x *= -1;
  if(ball_y - ball_r/2 <0) speed_y *= -1;
  rect(mouseX, mouseY, bar_w, bar_h);
  
  if((ball_x + ball_r/2 > mouseX && ball_x - ball_r/2 < mouseX+bar_w) 
    &&(mouseY < ball_y + ball_r/2 && ball_y + ball_r/2 < mouseY+bar_h)){
      speed_y *= -1;
  }
    
  for(int j=0;j<5;j++){
    for(int i=0;i<5;i++){
      // 生きていればブロックを描画する
      if(is_alive[i][j]){
        rect(i*block_w, j*block_h, block_w, block_h);
      }
      if((ball_x + ball_r/2 > i*block_w && ball_x - ball_r/2 < (i+1)*block_w) 
        &&(j*block_h < ball_y - ball_r/2 && ball_y - ball_r/2 < (j+1)*block_h)
        && is_alive[i][j]){ //条件式に「生きていれば」を追加
          speed_y *= -1;
          ball_y = ball_r/2 + (j+1)*block_h+1;
          is_alive[i][j] = false; // ブロックは消えた
      }
    }
  }
}

これで,ブロックが消えるようになりました!

以上で,最小限のブロック崩しは完成です.お疲れ様でした!

今後の展開

今回扱ったのは最小限のブロック崩しであり,少なくともこの記事を見た人は同じようなブロック崩しが完成します.

もっとちゃんとしたブロック崩しを作るなら,自分色をどう出していくかは考えないといけません.ここではそのアイデアを書き残します.

  • スタート画面やクリア画面を作る.
    これらはゲームのシーン遷移ということになるので,以下の記事が参考になるかもしれません.

gotutiyan.hatenablog.com

  • ブロックに耐久値を設定する.
    今は1回当たればブロックが消えますが,例えば2,3回当たれば消えるみたいなことです.

  • スコアを設定する.
    ブロック1つ消したら100点みたいな.

  • 時間制限を付ける.
    上記のスコアと絡めて,「60秒間で何点取れるか」みたいなシステムにすると面白いかも.

  • デザイン面を強化する.
    ボールに残像を残したり,上記の耐久値と絡めてあと何回叩けば良いのかを色で表したり,背景にちょっとアニメーションみたいなのをいれたり.

  • ブロックが増える?
    最初見えている範囲以外にも実はブロックがあって,右からどんどん追加されるとか.

他の最小限ゲーム制作シリーズ

今回はブロック崩しを扱いましたが,ほかにも同じような粒度で解説しているものがあります.よろしければご覧ください.

gotutiyan.hatenablog.com

gotutiyan.hatenablog.com

甲南大学知能情報学部の講義感想

はじめに

甲南大学の知能情報学部の講義について,学べることや感想を書いていきます.あくまでも,僕が受けた授業についてのみ書いていることにご注意ください.
また,講義の内容については,各講義のシラバスが最も正確であり,最も信憑性が高いということを明記します.

なお,記事の内容は僕が講義を受けた時点の内容なので,いまあなたが見ている年度では大きく異なる場合があります.そういう意味で,必ずシラバスを確認してください.

あと,4年の初めに書いているので,1年次の科目とか正直うろ覚です.それと基礎共通科目は書いてません.ごめんよ

1年次に受けたもの

体育

1限+六アイは朝がきつい.2限+六アイは帰ってくる時間がお昼の時間を侵食しがち.よって岡本の体育館になる種目が人気で,それを取れたら勝ちみたいなところあり.面白く無いけど出席さえすれば単位もらえる.

英語(読み)

GTECの結果でクラス分けされる.とは言っても,やる気のない人はわざと低い点を取って下のクラスに行こうとするため,普通にGTECをやればそれなりに上のクラスになる.僕はTS組だったが言うてもセンターをちょっと簡単にしたくらいの長文を読むだけなので,甲南の英語を解けて入学してきている人には,難しいものではないと思う.受験時の英語の感覚がある程度残っていれば簡単.

英語(聞き)

覚えてません,英語を聞きました.特に何もしてないけど取れた.

英語(話す)

今から時間とるのでN人と自己紹介しましょう系のタスクが無限に降ってくる.僕はそういうの好きではないのでご察し.(先生によって違いそう.)出席をしていたら取れた.

第二外国語(読み・話す)

中国語を取りました.読む方も話す方も,結局文を読むし発音するので,言うてやっていることは同じ.出席をしてテスト前に単語と文法を詰めれば取れた.

知能情報学概論及び基礎演習

知能情報学部でやる学問を一通り知ることができます.担当教員のもとで行う少人数ゼミでは,初めて研究室という場所で作業するので大学生感が増す.本当に概論なので,この講義で何か学べるかというと特になさそう.出席すれば取れた.

微積I

微積分をします.線形代数でも同じことが言えますが,学籍番号で先生が変わるので,人によって先生が変わります.先生との相性はある程度あるような気がするので,うまく適応しましょう.この学部の受験に数3の履修は必須ではないので,高校でもやったようなことを含みます(ただし,証明とかは高校よりちゃんとやっていくと思う).微積の前半であるこの講義では数3履修者は少々退屈するかも.良い復習という感じ.教科書の問題を自主的に一周すれば取れた.

微積

偏微分とか重積分とか,大学の微積って感じのことをします.授業を聞いていれば理解できると思います.教科書の問題を自主的に一周すれば取れた.

線形I

初めて行列に触れる体験をあなたに.線形代数をします.数学Cなんて今時の高等教育には無いので,初めての行列は楽しいのではないでしょうか.基本的には機械的な計算だけをやります.先生によってはその図形的な理解というか,演算がどういう意味を持つのかまで踏み込んでくれるかもしれません.教科書の問題と配布プリントをやると取れた.

線形Ⅱ

引き続き行列の演算をします.確か対角化とかをしたような.行列の固有値とそれに属する固有ベクトルを求められて,対角化できて,対角化した行列を利用してA^nを求めるみたいなテッパンの流れができれば,単位取得には十分だと思います.教科書の問題と配布プリントをやると取れた.

確率統計

平均・母集団・などの話からt検定,区間推定など.必修なこともあって,わかりやすくやってくれます.配布プリントの問題を復習すれば取れた.

プログラミング演習I

プログラミングをします.まずはUNIXコマンドをやってから,Processingという言語を学びます.UNIXコマンドは,この講義を受けている時には「こんなん何に使うねん」という感じですが,卒業研究を始めたあたりでその恩恵を知ることになると思います.Processingでは基本的な変数,if,forといった概念を学びます.関数の概念は確かやらなかったような・・・クラスの概念なんてもってのほかです...やって欲しいですけどね.簡単.

最後に最終課題というのがあって,自由に作品を作れます.そこで良いのを作ると,大学のホームページに掲載されます(以下のリンクから).僕は2017年度「青い四角の物語」の作者です.

www.konan-u.ac.jp

プログラミング演習Ⅱ

Processingを卒業してC言語に入ります.C言語はこの講義だけではなくて,2年次のアドバンスドプログラミングにもつながるので,できるだけ,つまづきたくないところです.とにかく自分で色々実装して感覚を掴みましょう.AtCoderとかすると良いんじゃ無いかな.簡単.

コンピュータサイエンス

THE・基本情報技術者試験な内容.N進数の計算,論理回路,よくある磁気ディスクにアクセスするための時間計算とか.情報系なら知っておきたい知識が得られます.なんで必修じゃないんやろねこれ.基本情報の勉強をしてる人なら取れて,そうでなくてもスライドに書いてある単語が分かれば取れる.

IT基礎

オフィスの練習.WordやExcelの使い方を学びます.僕はこの講義を受けたことで,パワーポイントで箇条書きする時にTabキーで段組みを作れることを知りました.簡単.

2年次に受けたもの

応用システム解析

システムの様子を微分方程式で記述し,微分方程式を主にラプラス変換で解くことを目指しています.ラプラス変換の前に区分線型標準化とかもやりますが,最終的にはラプラス変換がメインになります.うちの微積の講義は微分方程式にあまりフューチャーされないので,「微分方程式ってなんやねん」状態の人が多い印象です.その中でラプラス変換とやらを使って講義が進むので,よく分からないものをよく分からない手法で解く感じになってしまいました.個人的には難しかったです.

システム制御工学

応用システム解析が前期にあって,その続きの講義.後期にありました.やってることは応用システム解析とそないに変わらないので,応用システム解析が楽しかったり,ある程度わかっている人は引き続き取ると良いと思います.計算量が増えて,部分分数分解をひたすらしていたような気がします.難しい.

オペレーションズリサーチ

通称OR.線型計画問題とか動的計画法とかをやります.教科書もわかりやすくて,講義もわかりやすいのでオススメです.講義の最後に大きめのレポートがあって,講義に出てきたORの手法で実問題っぽいものを設定して解こう,というのが出ます.アイデアこそ出れば書くだけなので良いですが,アイデアが出ないと辛いかもしれません.僕は,数種類の科目に,勉強に必要なコストと得られる知識量を適当に定義して,あるコスト上限の元で知識量を最大化するための科目選択をする,というのを動的計画法で解いた話を書きました.テーマこそ誰でも思いつくようなものだったので,動的計画法の計算を手計算ではなくプログラムで行うことで差別化を図りました.個人的に好きな分野であることもあり,簡単.

データ構造とアルゴリズムI

Iではデータ構造が中心です.基本情報技術者でも頻出のスタック・キューとか,配列とリストの計算量の比較,木の探索などをやります.そこまで難しくないです.ポインタが分かってないとリストの説明されてもよく分からないので,ポインタは頑張りましょう.簡単.

データ構造とアルゴリズム

Ⅱではアルゴリズムが中心です.やっぱり最初はソートから始まります.(今時ソートなんて組み込み関数呼ぶだけなので,もっとやるべきことがあるような気もするけど..)その後,8クイーン問題とか三目並べのAIみたいなやつとかをやります.簡単.

確率統計学

確率統計をします.標本平均とかから始まって,t検定,区間推定など.各回で結構問題を解かせてくれるので,具体例で確認できて面白かったです.難易度は普通.言ってることがわからなくなってきたら,それより前に出てきた事がわかってない事が多いので,定期的に戻ろう.

オペレーティングシステム

OSのプロセス管理が主なテーマです.複数のタスクをコンピュータがどう処理しているのかということが学べます.内容は基本情報技術者にも直結します.例えば,この講義で出てきたラウンドロビン方式は,僕が受けた基本情報技術者試験の午後に大問として扱われていました.簡単め.

人工知能

僕が受けた年だけ諸事情で担当の先生が通常とは異なりましたが,基本的には探索手法をやりました.山登り法とか,ミニマックス法とか,Aスターとかです.いずれもゲームAI寄りという感じでした.機械学習の手法に興味がある人は面白いと思います.一つ一つの手法の考え方をちゃんと落とし込む作業をすれば取れる.

数値プログラミング技法

数値プログラミングという名前ですが,中身は行列演算です.僕が受けたときには,C++のEigenというライブラリを使って行列計算をひたすらやっていました.連立方程式を解いたり,〇〇分解と名のつく手法を使って問題を解いたりします.基本的に線形代数の講義で手計算でやっていたものを,プログラムでやるとこんな感じなんやなーと実感できます.ちょっと難しい.

コンピュータアーキテクチャ

コンピュータのメモリの話が大半です.あるデータを記憶したいときに,どのようにしてメモリ確保が行われるとか,キャッシュメモリはどのように動くのか,などです.個人的に内容が難しくて,後半はあまりついていけませんでしたが..

離散数学

簡単な集合の話や,命題,組み合わせ計算などをやります.内容は簡単だと思います.情報技術者試験でも求められる分野です.簡単.

アドバンスドプログラミング

プログラミング演習Ⅱに続く科目です.C言語を使って,構造体や関数などをやったような気がします.(あまり覚えていません.)そんなに難しくはないですが,課題がそれなりにめんどくさかったような.(なんか縦向きのヒストグラムとか書いたような..)簡単.

IT組織・管理

権利関係の法律や,企業のCSRなどの話題.なんかニュースを読んでその感想を書くというのを毎回やるので,その準備だけして,テスト前に黙って覚えること覚えれば単位がやってくる.

情報英語

Wepkipediaの"人工知能"の項目を丸々読んだりします.読みながら語彙をやったり,先生独自のプリントで,単語をそれなりに覚えます.個人的にあまりお勧めできないが,選択必修Aの枠の授業を埋めるにあたってコミュニケーション系を全て避けると必ず取ることになる授業.Wikipediaの文章を読むことにどれくらいの意義があ流のかよく分からないが,単位だけを考えるなら簡単.

情報セキュリティ

サーバーの役割や鍵を用いた暗号化,ネットワーク関係のセキュリティをやります.実習としては,chmodを用いたファイルやフォルダのパーミッションの変え方をやります.この講義の内容は他の広義には一切出てこないような気がするので,取って損なしです.難易度は鍵さえ理解できれば簡単な部類.

情報解析

ここでいう情報とは主に音声のことです.音声データは波形なので,その波を色々な三角関数の和で表すことができれば嬉しいみたいです.フーリエ変換を使えばそういうことができるので,フーリエ変換をやります.フーリエ変換にも離散的なやつと連続的なやつがあるようで,両方ともやります.僕は結構苦手でしたが,わかる人はすぐ理解できていたようです.

最適化

ある関数の値を最小化/最大化するような変数を求める作業を最適化と呼びます.例えば下に凸な二次関数を最小化するxは頂点を見れば良いわけですが,この講義ではそういうことをやります.ラグランジュの未定乗数法は感動ものですね.線形計画問題もやります.難易度は普通.授業で扱われたことを再確認していれば解ける(まあ任意の講義がこれなんですが).

データベース

データベースの扱いをやります.テーブル同士の結合とか,SQLです.情報技術者試験でも問われるところですね.演習としては実際にSQLでテーブルの作成と列/行の取り出しを行います.用語とSQLをきちんと覚えておけば取れます.

コンパイラインタプリタ

主に文法規則や構文解析の話です.文法規則は,プログラムには文法がありますが,それってどうやって定義されているの,みたいなことです.その手法としてBFN記法とかオートマトンをやります,これは簡単で楽しかったです.しかし構文解析関係が個人的に難しくて,あまり理解できませんでした.

オブジェクト指向プログラミング

クラスの概念を知ることができます.カプセル化や継承と行ったクラスの利点をやります.多相性の概念もやって,異なるクラスでも同じ関数を定義していれば,クラスのインスタンスを詰め込んだ配列をforで回して関数呼べるみたいなことができるみたいで,面白いです.Javaで実習します.簡単.

グラフ理論

グラフの話をやります.グラフはノードとエッジを用いて,ノード間をエッジで繋ぐように構成されます.主にこのノードとエッジの関係,具体的には次数とか彩色数とかです.グラフがオイラーグラフなら一筆書きできるみたいな話もあって,僕はこれを聞いて一筆書きのゲームを作りました.役立ちます.簡単.

3年次に受けたもの

集合と位相I

Iでは主に集合の話をします.位相はⅡらしいです.ε-δ論法とか,命題,写像関係です.個人的に集合論は興味があったので,面白く聞けました.テストは時間のわりに問題数が多いので,持ち込み資料を見ている暇はあまり無いと思います.難しさは普通.

幾何学I

コーヒーカップとドーナツは,トポロジーの世界では同じだそうです.そんな話です.色々な形のものをトポロジーの世界で貼り合わせたり分裂させたりすると,また違う形のものが生まれるみたいな話が続きます.(あまりちゃんと理解できてない気はする).テストをやる分にはそれなりに簡単だと思うんですが,学問としては僕はとても難しいと思います.

Webコンピューティング

検索エンジンで検索した時に,上位の検索結果はどのように得られているのかをやる感じです.その手法としてページランクとか,文書間の類似度としてJaccard係数とかTF-IDFとか出てきます.自然言語処理に近いところも多く,面白かったです.難易度は少し難しい.

ソフトウェア工学

開発を進める上での手法を主にやります.ウォーターフローとかアジャイルとか.各開発方法のメリットデメリットをやります.また.クラス図も書きます.クラス図に関してはhas-a, is-a関係を意識して書ければ問題ないです.それなりに簡単.

実験計画法

複数の要因が結果に影響するような実験において,どの要因が一番影響が大きいの,みたいなのをやった(はず).多重比較検定とか.そもそも何を求めようとしているのかがよく分からなくて,個人的には難しかった.

パターン認識

機械学習や深層学習につながるような内容.k-meansなどのアルゴリズムや,深層学習の基礎となるパーセプトロンの話,学習の結果を評価する指標など.機械学習に興味のある人はぜひ受けるべきです.普通な難易度.

知能化技術

機械学習や深層学習につながるような内容.パーセプトロンの話とか,モデルの層の話とか,学習の過程の話をやります.後は主成分分析とか回帰分析などの手法が,どのようなタスクに適応可能かをやります.機械学習に興味のある人はぜひ受けるべきです.普通な難易度.

自然言語処理

人が普段から書いている文章を自然言語と言いますが,それをどう処理するかをやります.この講義では最新の言語処理をやるというよりは,少し古めの技術についてやっています.ですが,そのような技術も決して無駄ということはないです(経験談).言語処理ではどのようなことが行われているのかを知りたい人は受けると良いと思います.難しめ.

画像工学

画像を処理するための工夫についてやります.実習形式で,matlabを使います.ランレングス符号化で画像を圧縮して表現するとか,アンチエイリアスで輪郭を良い感じにするとか,そういう感じです.普通.

経営情報システム

経営に関する様々な話題を扱っています.経営に関する情報の話題なので,特に数式などは出てこず,活用例を見ていく感じです.聞いていて面白かったです.簡単.

最適化プログラミング

matlabを使って,関数のグラフを書いたり,excelのソルバーを使って線形計画問題を解いたりします.matlabは画像工学の講義でも使うので,すでに受講していればこの講義でも有利です.線形計画問題も,2年次配当科目のオペレーションズリサーチでほぼ同じことをやるので,受講していれば有利です.普通な難易度.

【Pytorch】ミニバッチ学習に便利なDataSet・DataLoaderの使い方

はじめに

深層学習によって学習を行う際には,ミニバッチ化して学習させることが一般的です.本記事では,pytorchで提供されているDataSetとDataLoaderという機能を用いてミニバッチ化を実現する方法について書きます.

ミニバッチ化とは

簡単にミニバッチ化について復習します.ミニバッチ化とは,訓練データをそれなりに小さいサイズで分割し,それによって得られた小規模な訓練データ(ミニバッチ)で訓練することです.分割する際には,バッチサイズが必要です.例えばバッチサイズが2だと,全てのデータを2個ずつの組みに分けることになります.

より具体的な例を見てみます.与えられた数字が偶数か奇数かを判定するモデルを作るとしましょう.偶数だと0,奇数だと1を出力させます.いま,訓練データが

# 入力
X = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 出力(教師)
t = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]

のように与えられているとします.2つのリストの同じindexに格納されているデータが,入力と出力のペアになります.例えば,X[5]=5, t[5]=1なので,入力5に対する出力は1,つまり奇数であることが分かります.また,訓練データ数は10です.ここで,バッチサイズ=2でミニバッチ化を行うと,

# 入力1
[0, 1]
# 出力1
[0, 1]

# 入力2
[2, 3]
# 出力2
[0, 1]

# 入力3
[4, 5]
# 出力3
[0, 1]

# 入力4
[6, 7]
# 出力4
[0, 1]

# 入力5
[8, 9]
# 出力5
[0, 1]

のように分割されます.(ここでは,データをシャッフルするなどはせず,単純に先頭から区切っています.)このように小規模になったデータを用いて学習を回していきます.

蛇足になりますが,エポックの話をしましょう.今回の例では,データ数10に対してバッチサイズを2としたので,ミニバッチは5つ作成されます.このミニバッチを用いて学習するとき,ミニバッチ5つ分の学習を行えば,分割前のデータ数に相当する学習を行ったことになります.このように,分割前のデータ1回分の学習に相当する学習を1単位として1エポックと呼びます.

DataSetとDataLoader

さて,本題に入っていきます.
pytorchでは,DataSetとDataLoaderを用いることで,ミニバッチ化を簡単に実装できます.

DataSetは,元々のデータを全て持っていて,ある番号を指定されると,その番号の入出力のペアをただ一つ返します.クラスを使って実装します.

DataLoaderは,DataSetのインスタンスを渡すことで,ミニバッチ化した後のデータを返します.これはpytorchに元から用意されている関数を呼ぶだけなので,実装することはほぼありません.

まずはDataSetから実装します.

DataSetの実装

DataSetを実装する際には,クラスのメンバ関数として__len__()__getitem__()を必ず作ります.

__len__()は,len()を使ったときに呼ばれる関数です.
__getitem__()は,array[i]のように[ ]を使って要素を参照するときに呼ばれる関数です.これが呼ばれる際には,必ず何かしらのindexが指定されているので,引数にindexの情報を取ります.また,入出力のペアを返すように設計します.今回はカンマ区切りで返すことにしましょう.

冒頭でミニバッチ化を説明した時と同じように,入力値が偶数か奇数かを判定するモデルを想定します.

class DataSet:
    def __init__(self):
        self.X = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 入力
        self.t = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] # 出力

    def __len__(self):
        return len(self.X) # データ数(10)を返す

    def __getitem__(self, index):
        # index番目の入出力ペアを返す
        return self.X[index], self.t[index]

さて,実際にこのDataSetがどのような振る舞いをするか試してみましょう.

dataset = DataSet()
print('全データ数:',len(dataset))  # 全データ数: 10
print('3番目のデータ:',dataset[3]) # 3番目のデータ: (3, 1)
print('5~6番目のデータ:',dataset[5:7]) # 5~6番目のデータ: ([5, 6], [1, 0])

DataSetクラスのインスタンスが,len()を受け付けていて,[index]による参照も行えることが確認できます.また,[index]による参照では,入出力のペアが得られていることも確認します.例えば3番目のデータは(3, 1)なので,入力が3で,出力は1,つまり奇数であることが分かります.

DataLoaderの実装

次はDataLoaderです.これはpytorchのモジュールに含まれるtorch.utils.data.DataLoader()を使います.この関数の引数には, DataSetのインスタンスと,バッチサイズをbatch_size=で渡します.(他にもパラメータは指定できますが,後で紹介することにします.)

バッチサイズは2としましょう.

# さっき作ったDataSetクラスのインスタンスを作成
dataset = DataSet()
# datasetをDataLoaderの引数とすることでミニバッチを作成.
dataloader = torch.utils.data.DataLoader(dataset, batch_size=2)

これだけで,分割されたデータが得られています.分割後のデータは,for文で取り出すことができます.

for data in dataloader:
    print(data)

'''
出力:
[tensor([0, 1]), tensor([0, 1])]
[tensor([2, 3]), tensor([0, 1])]
[tensor([4, 5]), tensor([0, 1])]
[tensor([6, 7]), tensor([0, 1])]
[tensor([8, 9]), tensor([0, 1])]
'''

tensor()というのは,pytorchの独自の型です.計算グラフというのを保持するので,学習時に必ず変換する型です. 一応,これで最小限のミニバッチ化ができたので,学習を回すことができます.適当に10エポック学習するとすれば

epoch = 10
model = #何かしらのモデル
for _ in range(epoch):
    for data in dataloader:
        X = data[0]
        t = data[1]
        y = model(X)
        # lossの計算とか

などと書けるでしょう.また,

for data in dataloader:
    X = data[0]
    t = data[1]

は,素直に

for X, t in dataloader:

とも書けます.

shuffle

ここまでで得られたミニバッチの入力(X)に注目すると,先頭から順に0,1,2,3...となっています.この原因は,分割時に単に先頭から切り取っているからです.しかし,実際にはランダムに選んでくるのが望ましいです.そのようなとき,shuffle=Trueとすることで実現できます.

dataset = DataSet()
dataloader = torch.utils.data.DataLoader(dataset, batch_size=2, shuffle=True)
for data in dataloader:
    print(data)

'''
出力:
[tensor([4, 1]), tensor([0, 1])]
[tensor([0, 7]), tensor([0, 1])]
[tensor([9, 3]), tensor([1, 1])]
[tensor([6, 5]), tensor([0, 1])]
[tensor([8, 2]), tensor([0, 0])]
'''

順番がバラバラになりました.さらに,同じ変数であっても,取り出すたびに順番が変化します.

dataset = DataSet()
dataloader = torch.utils.data.DataLoader(dataset, batch_size=2, shuffle=True)
print('1回目')
for data in dataloader:
    print(data)
print()
print('2回目')
for data in dataloader:
    print(data)
print()
print('3回目')
for data in dataloader:
    print(data)

'''
出力:
1回目
[tensor([0, 3]), tensor([0, 1])]
[tensor([4, 7]), tensor([0, 1])]
[tensor([1, 9]), tensor([1, 1])]
[tensor([5, 8]), tensor([1, 0])]
[tensor([2, 6]), tensor([0, 0])]

2回目
[tensor([5, 9]), tensor([1, 1])]
[tensor([0, 2]), tensor([0, 0])]
[tensor([4, 1]), tensor([0, 1])]
[tensor([6, 8]), tensor([0, 0])]
[tensor([3, 7]), tensor([1, 1])]

3回目
[tensor([4, 2]), tensor([0, 0])]
[tensor([6, 0]), tensor([0, 0])]
[tensor([3, 1]), tensor([1, 1])]
[tensor([7, 8]), tensor([1, 0])]
[tensor([5, 9]), tensor([1, 1])]
'''

特に理由がなければ,Trueにしておくべき引数です.

drop_last

ここまでは,データ数10に対してバッチサイズが2でした.10は2で割り切れるので,綺麗に分割できました.ですが,バッチサイズを3にすればどうでしょうか.

dataset = DataSet()
dataloader = torch.utils.data.DataLoader(dataset, batch_size=3)
for data in dataloader:
    print(data)

'''
出力:
[tensor([0, 1, 2]), tensor([0, 1, 0])]
[tensor([3, 4, 5]), tensor([1, 0, 1])]
[tensor([6, 7, 8]), tensor([0, 1, 0])]
[tensor([9]), tensor([1])]
'''

このように,最後に1組だけ仲間はずれになります.仲間はずれのミニバッチは,様々な理由から使わない方が良いことがあります.このようなとき,drop_last=Trueを指定することで,仲間はずれを除去します.

dataset = DataSet()
dataloader = torch.utils.data.DataLoader(dataset, batch_size=3, drop_last=True)
for data in dataloader:
    print(data)

'''
出力:
[tensor([0, 1, 2]), tensor([0, 1, 0])]
[tensor([3, 4, 5]), tensor([1, 0, 1])]
[tensor([6, 7, 8]), tensor([0, 1, 0])]
'''

こちらも,特に理由がない限りはTrueにしておくと良いと思います.

なぜDataLoaderなのか

ミニバッチは,言ってしまえば[data[i*batch_size : (i+1)*batch_size] for i in range(len(data) / batch_size]みたいに,リストのスライスを使うだけでも実現できます.でもなぜDataLoaderを使うのかというと,個人的には以下の点が良いと思っています.

  1. shuffleやdrop_lastの引数が便利
  2. 分割後のデータをtensorで返してくれる
  3. ミニバッチ化してることが一目で分かる

特に2.について補足ですが,分割前のデータをリストで持っていても,numpyのndarrayで持っていても,とにかくtensorにして返してくれます.例えばpandasで入力を受けてモデルに流す時などはndarrayになると思いますが,分割前のデータ型をあまり気にしないで使用できるところは大きいと思います.

ndarrayでも動作することを確かめたい人は,プログラムの先頭でimport numpy as npを書いた上で,DataSetの

def __init__(self):
        self.X = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        self.t = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]

def __init__(self):
        self.X = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
        self.t = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])

に書き換えてみてください.

__getitem__()の返り値を辞書にする

この項目は蛇足かもしれませんが,せっかくなので書いておきます.

DataSetのところで,

def __getitem__(self, index):
        return self.X[index], self.t[index]

と書いていましたが,

def __getitem__(self, index):
        return {'X': self.X[index],
                't': self.t[index]}

とすることで,dataloaderから辞書型で取り出せます.

dataset = DataSet()
dataloader = torch.utils.data.DataLoader(dataset, batch_size=2)
for data in dataloader:
    print(data)
    # data['X']やdata['t']で参照可能に

'''
出力:
{'X': tensor([0, 1]), 't': tensor([0, 1])}
{'X': tensor([2, 3]), 't': tensor([0, 1])}
{'X': tensor([4, 5]), 't': tensor([0, 1])}
{'X': tensor([6, 7]), 't': tensor([0, 1])}
{'X': tensor([8, 9]), 't': tensor([0, 1])}
'''

一つの方法として,見やすくなると思います.

おわりに

今回は,DataSetとDataLoaderを用いてミニバッチ化する方法について詳しく説明しました.

参考になれば幸いです.

【python3.x】 練習問題55本ノック【問題と解答】

はじめに

ここではpythonの練習問題を掲載しています.

データを処理する際には,必ず「データの格納」と「データの取り出し」を行うことになるので,その方法を知ることを目的としています.問題は暗算で解けるものや,頭の中で答えが分かるものが多数ありますが,あくまでも,「行いたい処理をpython3という言語ではどのように表現するのか」を確認するものです.

このページは,問題とその解答が掲載されているページです.問題だけのページは以下のリンクからどうぞ.

gotutiyan.hatenablog.com

全ての問題には,必ず出力が存在します.出力はprint()を用いて,画面に表示させることを想定しています.

問題と解答

1. 変数

xという変数に2を代入し,それを3倍した数を出力してください.

期待する出力:6

解答

x = 2
print(3 * x)
2. swap

変数a100を代入し,変数b200を代入します.その後,両者の値を入れ替えて,a200b100が代入されているようにしてください.出力としてはabを出力してください.出力形式は問いませんが,print(a,b)とすると,空白区切で出力できます.

期待する出力の一例:200 100

解答

a = 100
b = 200
a, b = b, a
print(a, b)
  • 1つの代入式で複数の変数を扱えます.
3. 四則演算+\alpha

変数a10を代入,変数b2を代入し,abの和,差,積,商を出力してください.出力形式は問いません.また,商の値が小数か整数かは問いません.

期待する出力の一例:12 8 20 5.0

解答

a = 10
b = 2
print(a+b, a-b, a*b, a/b)
  • 割り算は,/だと結果が小数で,//だと結果が整数で得られます.
4. 余り

変数a5を代入,変数b3を代入し,abで割った時の余りを求めてください.

期待する出力:2

解答

a = 5
b = 3
print(a%b)
5. べき乗

変数a5を代入,変数b10を代入し,ab乗,つまりa^bを出力してください.

期待する出力:9765625

解答

a = 5
b = 10
print(a**b)
6. if,比較演算子<, >

変数a5を代入,変数b10を代入し,abのうち大きい方を出力してください.

期待する出力:10

解答

a = 5
b = 10
if a < b:
    print(b)
else:
    print(a)
  • ifやforを書く際には,どこまでがその対象かを表すブロックを作る必要がありますが,pythonではインデントによってこれを示します.
7. 比較演算子==, bool

変数a5を代入し,aが偶数ならTrue,そうでなければFalseを出力してください.(条件式をそのままprintすれば良いです.)

期待する出力:False

解答

a = 5
if a % 2 == 0:
    print(True)
else:
    print(False)
# もしくは
print(a%2 == 0)
  • 比較演算子==, !=, <, >, >=, <=)はbool型と呼ばれるTrueFalseのいずれかを返します.
8. 文字列(ランダムアクセス)

変数に,'python'という文字列を代入し,先頭の文字を0番目として2番目の文字を出力してください.

期待する出力:t

解答

s = 'python'
print(s[2])
  • 文字列は,[ ]を使うことで,文字を抽出できます.
  • [2]のように,[ ]の中に指定する数値を添え字(index)と呼びます.
9. 文字列(結合)

'py''thon'という2つの文字列をそれぞれ変数に代入し,結合したものを出力してください.

期待する出力:python

解答

s1 = 'py'
s2 = 'thon'
print(s1 + s2)
  • 文字列は,+を用いることで結合できます.
10. 文字列(format)

変数a5を,変数b3を代入し,これらの変数を用いて'5%3=2'という文字列を出力してください.

期待する出力:5%3=2

解答

a = 5
b = 3
ans = "{}%{}={}".format(a, b, a%b)
print(ans)
  • {}の記号を含んだ文字列に対して,.format()で数値や文字列を入れられます.
11. 文字列(replace)

変数に文字列'some1'を代入し,この文字列中の1oneに変換してください.

期待する出力:someone

解答

s = 'some1'
s = s.replace('1', 'one')
print(s)
12. 文字列(lower)

変数に文字列'This Is A Sentence .'を代入し,この文字列を全て小文字に変換してください.

期待する出力:this is a sentence .

解答

s = 'This Is A Sentence .'
s = s.lower()
print(s)
  • .lower()を用いることで,全ての文字が小文字になります.
13. 文字列(upper)

変数に文字列'This Is A Sentence .'を代入し,この文字列を全て大文字に変換してください.

期待する出力:THIS IS A SENTENCE .

解答

s = 'This Is A Sentence .'
s = s.upper()
print(s)
  • upper()を用いることで,全ての文字が大文字になります.
14. 文字列(文字数)

変数に文字列'How many characters?'を代入し,この文字列の文字数を出力してください.空白も含めるものとします.

期待する出力:20

解答

s = 'How many characters?'
print(len(s))
  • 文字列の長さはlen()で見ることができます.
  • len()は,頻繁に使用する汎用的なメソッドで,後述するリストの長さを得るときにも使います.
15. 文字列 → 数値

変数aに文字列'34'を代入し,変数bに文字列'43'を代入し,これらを数字と見なした時の和を出力してください.

期待する出力:77

解答

s1 = '34'
s2 = '43'
print(int(s1) + int(s2))
  • 文字列をint()で囲むことにより,数値に変換できます.これにより,四則演算などができます.
16. リスト

変数にリスト[1,2,3,4,5]を代入し,(先頭を0番目として)3番目の要素を出力してください.

期待する出力:4

解答

li = [1,2,3,4,5]
print(li[3])
  • 文字列と同じように,前から3番目の要素を見るときには[ ]を使用します.
  • 文字列は,リストの各要素が文字になっているものだと思うと良いかもしれません.
17. リスト(結合)

変数li1[1,2,3]のリストを代入,変数li2[4,5]のリストを代入し,2つのリストを結合して出力してください.

期待する出力:[1,2,3,4,5]

解答

li1 = [1,2,3]
li2 = [4,5]
print(li1 + li2)
18. リスト(append)

変数にリスト[1,2,3,4,5]を代入し,このリストの末尾6,7を1つずつ順番に追加してください.その後,最終的なリストを出力してください.

期待する出力:[1,2,3,4,5,6,7]

解答

li = [1,2,3,4,5]
li.append(6)
li.append(7)
print(li)
19. リスト(insert)

変数にリスト[1,2,3,4,5]を作成し,先頭を0番目としたときに,0番目と1番目の間に100を挿入してください.

期待する出力:[1,100,2,3,4,5]

解答

li = [1,2,3,4,5]
li.insert(1, 100)
print(li)
20. リスト(forによる捜査)

変数に[1,2,3,4,5]の5つの要素を持つリストを格納して,偶数の要素だけ出力してください.出力形式は問いません.

期待する出力:2 4

解答

li = [1,2,3,4,5]
for elem in li:
    if elem % 2 == 0:
        print(elem)
  • for 変数 in リストとすることで,[ ]を用いることなく,要素を先頭から順番に見ることができます.
21. リスト(forによる捜査, enumerate)

変数に[1,2,3,4,5]の5つの要素を持つリストを格納して,添え字が偶数番目の要素だけ出力してください.出力形式は問いません.ただし,先頭の添え字を0番目とします.

期待する出力:1 3 5

解答

li = [1,2,3,4,5]
for idx, elem in enumerate(li):
    if idx % 2 == 0:
        print(elem)
  • enumerate(リスト)を用いることで,リストの要素と共にその添え字も得られます.
  • 添え字と要素の組みが得られるので,それを受け取る変数も2つ書きます.ここではidxに添え字,elemに要素が順次代入されます.
22. リスト(len)

変数にリスト[11,22,33,44,55,66]を代入し,リストの要素数を出力してください.

期待する出力:6

解答

li = [11,22,33,44,55,66]
print(len(li))
23. リスト,if(存在確認)

リスト[11,22,33,44,55]に,値44が存在するかどうかをifを用いて判定してください.存在すればTrue,そうでなければFalseを出力します.

期待する出力:True

解答

li = [11,22,33,44,55,66]
if 44 in li:
    print(True)
else:
    print(False)
  • if 要素 in リストとすることで,値がリストに存在した時のみ発動する処理が書けます.
  • for 要素 in リストと形が似ていますが,forとifで意味が全然異なるので注意してください.
24. タプル,リストの負の添え字

変数に[1,2,3,4,5]の5つの要素を持つリストを格納して,リストの先頭の要素と末尾の要素をタプルとして格納し,出力してください.

期待する出力:(1, 5)

解答

li = [1,2,3,4,5]
tup = (li[0], li[-1])
print(tup)
  • タプルはリストと似ていますが,一度値を代入すると,2度と変更できない点が特徴です.
  • リストは[ ]で囲みますが,タプルは( )で囲みます.
  • 添え字に負の値を用いると,「末尾から何番目」を指定できます.末尾から-1, -2...です.
25. 辞書

変数に,値の組みとして{'A':1, 'B':2, 'C':3, 'D':4, 'E':5}を持つ辞書を格納して,出力してください.

期待する出力:{'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5}

解答

d = {'A':1, 'B':2, 'C':3, 'D':4, 'E':5}
print(d)
  • 辞書はキーと値(バリュー)を持つデータ構造です.
  • キーと値の型はなんでも良く,ここではキーに文字列,値に数としています.
26. 辞書(keys)

変数に,値の組みとして{'A':1, 'B':2, 'C':3, 'D':4, 'E':5}を持つ辞書を格納して,キーを要素としたリストを作成し,出力してください.

期待する出力:['A', 'B', 'C', 'D', 'E']

解答

d = {'A':1, 'B':2, 'C':3, 'D':4, 'E':5}
print(list(d.keys()))
  • .keys()とすることで,登録されている全てのキーが得られます.
  • .keys()の時点では特殊な型をしているので,リストにするには明示的にlist()で囲みます.
27. 辞書(values)

変数に,値の組みとして{'A':1, 'B':2, 'C':3, 'D':4, 'E':5}を持つ辞書を格納して,値(バリュー)を要素としたリストを作成し,出力してください.

期待する出力:[1,2,3,4,5]

解答

d = {'A':1, 'B':2, 'C':3, 'D':4, 'E':5}
print(list(d.values()))
  • .values()とすることで,登録されている全ての値が得られます.
  • .values()の時点では特殊な型をしているので,リストにするには明示的にlist()で囲みます.
28. 辞書(items)

変数に,値の組みとして{'A':1, 'B':2, 'C':3, 'D':4, 'E':5}を持つ辞書を格納して,キーと値(バリュー)の組みのタプルを要素としたリストを作成し,出力してください.

期待する出力:[('A', 1), ('B', 2), ('C', 3), ('D', 4), ('E', 5)]

解答

d = {'A':1, 'B':2, 'C':3, 'D':4, 'E':5}
print(list(d.items()))
  • .items()とすることで,登録されている全てのキーと値の組が得られます.
  • .items()の時点では特殊な型をしているので,リストにするには明示的にlist()で囲みます.この時,キーと値の組はタプルで表されます.
29. 辞書(キーの存在確認)

今,以下のように辞書が作成済みです.

d = {'apple':10, 'grape':20, 'orange':30}

この辞書に対して,'apple'というキーが存在するかを確認し,存在しなければ,'apple'というキーに対して-1という値を追加してください.また,同様のことを'pineapple'でも行なってください.その後,最終的な辞書を出力してください.

期待する出力:{'apple': 10, 'grape': 20, 'orange': 30, 'pineapple': -1}

解答

d = {'apple': 10, 'grape': 20, 'orange': 30, 'pineapple': -1}
d['apple'] = d.get('apple', -1)
d['pineapple'] = d.get('pineapple', -1)
print(d)
  • .get(キー)は,辞書にキーが既に登録されているかどうかを調べます.もし登録されていればキーに対応する値を,そうでなければNoneを返します.
  • .get(キー, 初期値)のように第二引数を与えると,キーが登録されていなかった場合に自動でそのキーを登録します.この時,対応する値は第二引数になります.
30. スライス1

変数に文字列'training'を代入し,先頭を0番目として,1番目から4番目までを取り出して出力してください.

期待する出力:rain

解答

s = 'training'
print(s[1:5])
  • 添え字を[a:b]とすると,[a, b)の範囲で切り取られます.ここでbは開区間なので,実際には含まれません.このような表記をスライスと呼びます.
31. スライス2

変数に文字列'understand'を代入し,先頭を0番目として,奇数番目の文字列だけを取り出した文字列を出力してください.

期待する出力:nesad

解答

s = 'understand'
print(s[1::2])
  • [a:]のように書いた場合,開始の添え字はaで,終了の添え字は末尾になります.(正確には末尾+1です.とにかく最後までの範囲を取ってくれます.)
  • [a:b:c]のように書いた場合,cは間隔を表します.具体的には,c-1飛ばしになります.上記の例だと,添え字2から1つ飛ばしで末尾まで,という指示になります.
32. スライス3

変数にリスト[1,2,3,4,5]を格納して,これを逆順に出力してください.

期待する出力:[5,4,3,2,1]

解答

li = [1,2,3,4,5]
li = li[::-1]
print(li)
  • 間隔として-1を設定すると,先頭に向かって1つずつ戻ることになります.上記のように全範囲を指定した時,最初末尾に飛んで,そこから先頭に戻るような挙動をするので,結果として逆順に見ることと等しくなります.
33. 集合

変数にリスト[1,1,2,3,3,4,5]を代入し,このリストを集合に変換して出力してください.

期待する出力:{1,2,3,4,5}

解答

li = [1,1,2,3,3,4,5]
se = set(li)
print(se)
  • リストをset()で囲むと集合になります.
  • 集合は,要素の重複を許しません.元のリストに同じ要素が複数存在しても,1つしか残りません.
34. 積集合

変数set1に集合{1,2,3,4,5},変数set2に集合{3,4,5,6,7}を代入し,積集合を出力してください.

期待する出力:{3,4,5}

解答

set1 = {1,2,3,4,5}
set2 = {3,4,5,6,7}
print(set1 & set2)
35. 和集合

変数set1に集合{1,2,3,4,5},変数set2に集合{3,4,5,6,7}を代入し,和集合を出力してください.

期待する出力:{1,2,3,4,5,6,7}

解答

set1 = {1,2,3,4,5}
set2 = {3,4,5,6,7}
print(set1 | set2)
36. 差集合

変数set1に集合{1,2,3,4,5},変数set2に集合{3,4,5,6,7}を代入し,差集合を出力してください.

期待する出力:{1,2}

解答

set1 = {1,2,3,4,5}
set2 = {3,4,5,6,7}
print(set1 - set2)
37. 型の確認

3つの変数に以下のように代入したコードがあります.各変数の型を出力してください.出力形式は問いません.

data1 = {'A':1, 'B':2}
data2 = "hoge"
data3 = {1,2,3,4,5}

期待する出力:<class 'dict'> <class 'str'> <class 'set'>

解答

data1 = {'A':1, 'B':2}
data2 = "hoge"
data3 = {1,2,3,4,5}
print(type(data1), type(data2), type(data3))
  • type()を用いることで,データの型を確認できます.
38. strip

文字列'This is sentence .\n'の改行記号を消したものを出力してください.

期待する出力:This is sentence .

解答

s = 'This is sentence .\n'
print(s.rstrip())
  • rstrip()は,文字列の末尾にある空白や改行記号を削除します.rはrightのrです.
  • 他にも,両端を処理するstrip()や,先頭だけを処理するlsrtip()もあります.
39. split

変数に文字列'C C++ // python java'を代入し,この文字列を空白で区切った後のリストを出力してください,また,同じことを'/'でも行い,出力してください.

期待する出力:['C', 'C++', '//', 'python', 'java'],['C C++ ', '', ' python java']

解答

s = 'C C++ // python java'
print(s.split())
print(s.split('/'))
  • splitは,文字を指定して文字列を分割するものです.何も指定しなければ,空白で分割されます.
  • 分割の結果はリストで得られます.
40. join

変数にリスト['This', 'is', 'a', 'sentence']を代入し,これらを空白区切で結合した文字列を出力してください.

期待する出力:This is a sentence

解答

words = ['This', 'is', 'a', 'sentence']
sent = ' '.join(words)
print(sent)
  • joinは,リストの要素を結合するものです.'結合文字'.join(リスト)という形で使います.
  • 今回は結合文字として空白を用いていますが,例えば'---'.join(words)とすればThis---is---a---sentenceが得られます.
41. max

変数にリスト[11,2,7,13,5]を代入し,最大値を出力してください.

期待する出力:13

解答

li = [11,2,7,13,5]
print(max(li))
42. min

変数にリスト[11,2,7,13,5]を代入し,最小値を出力してください.

期待する出力:2

解答

li = [11,2,7,13,5]
print(min(li))
43. sum

変数にリスト[11,2,7,13,5]を代入し,総和を出力してください.

期待する出力:38

解答

li = [11,2,7,13,5]
print(sum(li))
44. 昇順ソート

変数にリスト[5,3,1,4,2]を格納し,昇順に並び替えたリストを出力してください.

期待する出力:[1,2,3,4,5]

解答

li = [5,3,1,4,2]
sorted_li = sorted(li)
print(sorted_li)
  • sorted()を用いれば,リストをソートできます.デフォルトでは昇順ソートです.
45. 降順ソート

変数にリスト[5,3,1,4,2]を格納し,降順に並び替えたリストを出力してください.

期待する出力:[5,4,3,2,1]

解答

li = [5,3,1,4,2]
sorted_li = sorted(li, reverse=True)
print(sorted_li)
  • パラメータとしてreverse=Trueを指定すれば降順ソートになります.
  • 他にも,パラメータkeyを指定すれば,柔軟なソートが可能になります.(これは演習2で詳しく)
46. range()

0から99の100の要素からなるリストを出力してください.

期待する出力:[0,1,2....,99](一部省略)

解答

li = list(range(0,100))
print(li)
  • range()は指定の範囲で整数列を生成しますが,そのままでは特殊な型なので,リストにするには明示的にlist()で囲みます.
47. 内包表記

変数にリスト[5,4,3,2,1]を代入し,要素と添字の値(先頭を0とする)を足し合わせた数値を要素に持つリストを新たに作成してください.
内包表記を使うと便利です.

期待する出力:[5,5,5,5,5]

解答

li = [5,4,3,2,1]
ans_li = [idx + elem for idx, elem in enumerate(li)]
print(ans_li)
  • 内包表記は,「あるリストから別のリストを作る」ときに便利です.特に使わなくても困ることはないですが,場合によってはスマートに書けるので重宝されます.
48. 例外処理

下記のように変数が作られています.

a = 0
b = 5

この時,\frac{a}{b}\frac{b}{a}を出力してください.ただし,ゼロ割りが発生した時にはzero divisionを出力します.
例外処理を用いて書いてみると良いと思います.

期待する出力:0, zero division

解答

a = 0
b = 5

try:
    print(a / b)
except ZeroDivisionError:
    print('zero division')

try:
    print(b / a)
except ZeroDivisionError:
    print('zero division')
  • 例外処理は,try ~ exceptで表現されます.tryには行いたい処理を書いて,その処理を行う上で回避したいエラーがあれば,exceptに回避策を書きます.
  • pythonはゼロ割を検知するとZeroDivisionErrorを返す(raiseされる)ので,except ZeroDivisionError:となります.他にも,配列外参照ならIndexErrorがraiseされるので,except IndexError:となります.
49. ビット演算

変数a10を,変数b5を代入します.この時,ab論理和論理積排他的論理和を出力してください.出力形式は問いません.

期待する出力:15 0 15

解答

a = 10
b = 5
print(a|b, a&b, a^b)
  • ビット演算は,各ビットに対して演算が行われます.
  • 10進数 -> 2進数とする時,10 -> 1010, 5 -> 0101なので,例えば論理和を取ると1111となり,これは15です.
50. モジュールのインポート

mathモジュールをインポートして,\theta = \frac{\pi}{2}の下で,sin\theta^2 + cos\theta^2 = 1.0の値を出力してください.

期待する出力:1.0

解答

import math
theta = math.pi / 2
ans = math.sin(theta)**2 + math.cos(theta)**2
print(ans)
  • モジュールのインポートは,プログラムを書く上では欠かせません.
  • モジュールは山ほど存在し,用途によって使い分けます.有名なものだとnumpymatplotlibなどがあります.
演習1(forのネスト)

1から31までの整数を要素とするリスト1と,1から12までの整数を要素とするリスト2を作成します.リスト1とリスト2から値を1つずつ取ってきた時,1の位が一致する値が何組あるかを求めてください.例えば,111は,1の位が両方1なので,カウントに入ります.11311なども同様です.

期待する出力:38

解答

li1 = list(range(1, 32))
li2 = list(range(1, 13))
counter = 0
for elem1 in li1:
    for elem2 in li2:
        if elem1%10 == elem2%10:
            counter += 1
print(counter)
  • 2つのリストから抽出できる組を全て調べれば良いです.1の位が等しいかどうかは,10で割った余りを見れば判定できます.
  • for文は何重にでも重ねることができ,重ねることをネストすると呼ぶときもあります.
演習2(辞書の値ソート)

辞書が以下のように定義されています.

dic = {'two':324, 'four':830, 'three':493, 'one':172, 'five':1024}

この時,値で昇順ソートしたと考えた場合のキーの並びを出力してください.

期待する出力:['one', 'two', 'three', 'four', 'five']

解答

dic = {'two':324, 'four':830, 'three':493, 'one':172, 'five':1024}
items_list = list(dic.items())
sorted_items_list = sorted(items_list, key=lambda x:x[1])
ans = [elem[0] for elem in sorted_items_list]
print(ans)
  • この演習では,valuesのソート時に,keysの並びも変えるところが厄介です.items()で一旦タプルのリストにしてから,タプルの2つ目の値でソートするようにします.これにはkey=のパラメタを利用します.key=にはラムダ式を使って,各要素のどこに注目てソートするかを指定します.
演習3(num2freq)

リストが以下のように定義されています.

nums = [1,2,4,3,2,1,5,1]

このリストに対して,要素の出現数を格納した辞書num2freqを作成し,出力してください.これはnum2freq[要素] = 出現数となるような辞書で,例えばnum2freq[1] = 3です.

期待する出力:{1: 3, 2: 2, 4: 1, 3: 1, 5: 1}

解答

nums = [1,2,4,3,2,1,5,1]
num2freq = {}
for num in nums:
    num2freq[num] = num2freq.get(num, 0) + 1

print(num2freq)
  • 辞書を作成し,要素をキーとしたときの値を順次増やします.
演習4(word2freq)

今,文字列が以下のように与えられています.

doc = 'i bought an apple .\ni ate it .\nit is delicious .'

\nは改行記号なので,3つの文が3行に渡って書かれていることになります.
この文章中の単語を用いて,キーとして単語,値として出現数を持つような辞書word2freqを作成し,出力してください.ただし,改行記号は単語に含めないでください.
ヒント:改行記号でsplitしてから空白でsplitすれば,単語に分割できます.

期待する出力:{'i': 2, 'bought': 1, 'an': 1, 'apple': 1, '.': 3, 'ate': 1, 'it': 2, 'is': 1, 'delicious': 1}

解答

doc = 'i bought an apple .\ni ate it .\nit is delicious .'
word2freq = {}
sents = doc.split('\n')
for sent in sents:
    words = sent.split()
    for word in words:
        word2freq[word] = word2freq.get(word, 0) + 1

print(word2freq)
  • ヒントの通りsplitすれば,単語のリストが得られます.各単語をキーとして,対応する値を順次増やします.
演習5(Jaccard係数)

2つの集合A,Bの類似度を計算する方法として,Jaccard係数というものがあります.例えば,文章の類似度の指標として使われています.

Jaccard(A,B) = \frac{|A \cap B|}{|A \cup B|} = \frac{AとBの積集合の要素数}{AとBの和集合の要素数}

2つのリストが与えられている時,リストを集合と見なした時のJaccard係数の値を求めてください.

list1 = [12,23,34,45,56,67,78,89]
list2 = [21,32,43,45,65,67,78,98]

期待する出力:0.23076923076923078

解答

list1 = [12,23,34,45,56,67,78,89]
list2 = [21,32,43,45,65,67,78,98]

A = set(list1)
B = set(list2)
inter_set = A.intersection(B)
union_set = A.union(B)
print(len(inter_set) / len(union_set))
  • リストを集合に変換した後,Jaccard係数の計算式の通りに計算します.要素数len()で見れば良いです.