ホーム » スタッフ » 斉藤徹

斉藤徹」カテゴリーアーカイブ

2018年6月
« 5月    
 12
3456789
10111213141516
17181920212223
24252627282930

最近の投稿(電子情報)

アーカイブ

カテゴリー

前期中間テストの答案返却

前期中間試験の返却と解答の解説を行う。

今年の学生は、3年で私がプログラミング応用を担当していないので、私が例年3年に出題していた以下のような問題が苦手だろうということで、出題した問題。ポインタと構造体と配列が絡む問題は、式の意味を理解するためにも、C言語の演算子の優先順位などを踏まえ、こういった問題を知っておくべき。

また、上記のような問題では、C言語でのポインタと配列の意味を理解してもらうために、例年説明する極端なコーディングも紹介しておいた。

int a[5] = { 11 , 22 , 33 , 44 , 55 } ;
int* pi ;
pi = a + 2 ;
printf( "%d¥n" , *pi ) ;        // *( pointer + offset )
printf( "%d¥n" , pi[0] ) ;      // と pointer[ offset ] は同じ
printf( "%d¥n" , *(pi + 1) ) ;  // 44
printf( "%d¥n" , pi[ 1 ] ) ;    // 44
printf( "%d¥n" , (-1)[ pi ] ) ; // 22
printf( "%c¥n" , "abcdef"[ 2 ] ) ; // c

「 printf( “%d¥n” , -1[ pi ] ) ; 」の結果が -44 になった。一瞬なんでや…って思ったけど、-( 1[pi] ) なのね。

数学解答システムWolframAlphaが日本語化

数学に特化した、Web質問・解答システムのWolframAlpha が日本語化されました。

2x^2+3y^2=2, 2x+y= -1」と入力するだけで、方程式を解くだけでなく、円と直線の交点を求めるといった数学的意味が分かるグラフを表示してくれる。# すげー

Google でも、y=3x^2-2x+1 みたいな式を入力すると、グラフ表示をしてくれますが、もっと複雑な式でも処理できます。

z = sin(x^2+y^2)」と入力すれば、3次元プロットまで… # すげー

integrate 1/x dx 」で不定積分や、「y = diff( x^5+x^3+1 , x ) 」で微分なんてあたりまえ。低学年の数学の授業でグラフ電卓とか使っているけど、その機能以上のことが、スマホでもできてしまう。

「ステップごとの解説」というボタンもあり、数学の授業レベルの解説もしてくれる….とはいえ、これは WolframAlpha Pro の機能で有料。

( (1-cos(t)) cos(t) , sin(t)(1-cos(t)) ) でカージオイド(心臓形)まで表示してくれらぁ。

様々な移動平均の課題

移動平均のレポートでは、表計算ソフトを用いて、移動平均の範囲のとり方などを変えながら、平均をとった結果に、どう影響するのかを考える。

Excel で様々な移動平均の式を入力

表計算ソフトに、移動平均の式を入力する際には、以下の資料を参考にせよ。

上図のB4〜E4にできた移動平均の式は、B5以下にコピーすればいい。

レポート内容

以下のような移動平均を、Excel にて計算し、その結果の違いについて考察せよ。

  1. 単純移動平均(n=5) 前後11点の移動平均
  2. 単純移動平均(n=10) 前後21点の移動平均
  3. 過去の値による単純移動平均(n=5) 前6点の移動平均
  4. 過去の値による単純移動平均(n=10) 前11点の移動平均
  5. 加重移動平均(n=5) 前5点の加重移動平均
  6. 加重移動平均(n=10) 前10点の加重移動平均
  7. 指数移動平均(α=1/2)
  8. 指数移動平均(α=1/4)

平均に用いる値は、以下のデータとする。

    • 2018-06-05-wave.csv 時刻tとx(t)のコンマ区切りファイル
    • 時間の遅延がわかるような波形を用い、考察してあることが望ましい

1枚のグラフの中に、元波形+8波形=9本のグラフを記載すると、グラフの内容が分かりにくいので、複数のグラフ結果で図示すること。

プログラミングが得意な人は、上記をExcelで処理するのではなく、C言語にて移動平均を計算し、結果をExcelに取り込んでグラフとして表示することにチャレンジしてほしい。

提出レポートに、全データの計算結果は不要です。動作の確認の意味で、先頭10点ほどの計算結果をつけてください。

様々な移動平均

前回説明を行った単純移動平均は、時刻tの平均を、その前後のデータで平均を求めた。この方式は、実際には与えられた波形のデータを全部記録した跡に、単純移動平均をとる場合に有効である。

しかし、時々刻々変化する測定値の平均をその都度使うことを考えると、上記の方法は、未来の測定値を使っていることから、現実的ではない。

過去の値だけを使った移動平均

そこで、過去の値だけで移動平均をとることも考えられる。

この、単純移動平均と、過去の値だけを使う単純移動平均を、適当な測定値に対して適用した場合のグラフの変化を Excel によってシミュレーションした結果を以下に示す。

しかし、このグラフを見ると、波形後半の部分に注目するとよく分かるが、過去の値だけを使った移動平均では、測定値が立ち上がったのを追いかけて値が増えていく。これでは移動平均は時間的な遅れとなってしまう。

加重移動平均

過去の値を使った移動平均では遅れが発生する。でも、平均を取る際に、「n回前の値」と「現在の値」を考えた時、「その瞬間の平均値」は「現在の値」の方が近い値のはず。であれば、平均を取る時に、「n回前の値は少なめ」「現在の値は多め」に比重をかけて加算する方法がある。

この様に、過去に遡るにつれ、平均をとる比重を直線的に小さくしながら移動平均をとる方法は、加重移動平均と呼ばれる。以下にその変化をExcelでシミュレーションしたものを示す。

指数移動平均

ここまで説明してきた、単純移動平均や、加重移動平均は、平均をとる範囲の「過去の値」を記憶しておく必要がある。広い時間にわたる移動平均をとる場合は、それに応じてメモリも必要となる。これは、組み込み型の小型コンピュータであれば、メモリが足りず平均処理ができない場合もでてくる。

そこで、荷重移動平均の重みを、は、100%,は50%,は25%… というように、過去に遡るにつれ、半分にして平均をとる。

しかし、以降の項で、 を使うと以下のように書き換えることができる。

この方法であれば、直前の平均値を記録しておくだけで良い。このような移動平均は、指数移動平均と呼ばれる。

ここで示した指数移動平均は、過去を遡るにつれとなっているが、これをさらに一般化した指数移動平均は、以下の式で示される。前述の移動平均は、とみなすことができる。

 

K-SEC セキュリティサマースクールin 岐阜高専

夏休みに開催される、高専学生対象のセキュリティに関するサマースクールのお知らせです。

  1. 期日 平成30年8月23日(木)・24日(金)
  2. 会場 岐阜工業高等専門学校 専攻科棟2 階 第1講義室・第2講義室
  3. 内容

    学生の情報セキュリティに関する知識とスキルの向上と、情報セキュリティ教育に関わる教員の研鑽を目的として、2日間で情報セキュリティ高度人材育成に関するサマースクールを実施する。

    1日目【8月23日(木)】
    09:30 – 10:00 受付
    10:10 – 10:20 挨拶
    10:30 – 12:00 講座A/講座B
    12:00 – 13:00 昼休み
    13:00 – 14:30 講座C/講座D
    14:40 – 16:10 講座E/講座F
    2日目【8月24日(金)】
    08:30 – 08:45 受付
    08:50 – 10:20 講座G/講座H
    10:30 – 12:00 講座I/講座J
    12:10 – 12:40 クロージング
  4. 参加対象学生

    各国公私立高等専門学校の本科学生および専攻科生(定員:40 名)

  5. 見学対象教職員

    全国各校の情報セキュリティ教育担当者、または情報セキュリティ教育にご興味のある方

福井高専の参加希望者は、6/29までに斉藤に連絡をください。

関数ポインタの応用と仮想関数による実装

関数ポインタでアルゴリズムの汎用化

関数ポインタを利用すれば、異なるデータに対する処理を、 汎用性高く作ることも可能となる。 例えば以下の vmax() 関数は、自分で用意した大小を比較するだけの関数を渡し、 それ以外の最大値を求めるための処理を行う。 このため、他のデータの最大値を求めたい場合でも、最大値を求める処理を すべて記載するのではなく、対象データの比較関数だけを記述すれば良い。

int intcmp( int* x , int* y ) { // 整数比較関数
   if ( *x > *y )
      return 1 ;
   else if ( *x < *y )
      return -1 ;
   else
      return 0 ;
}
int vmax( void* array ,    // 配列先頭アドレス
          int size ,       // 配列データ件数
          int sizeofdata , // 1件あたりのbyte数
          int(*f)( void*,void* ) ) { // 比較関数
   int i , max = 0 ;
   for( i = 0 ; i < size ; i++ )
      if ( (*f)( array + max * sizeofdata ,
                 array + i   * sizeofdata ) )
         max = i ;
   return max ;
}
int idata[ 4 ] = { 11 , 33 , 22 , 44 } ;
char sdata[ 4 ][ 4 ] = { "ab" , "bc" , "aa" , "c" } ;
void main() {
   int m ;
   // intcmp関数を使って、idata から最大値を探す
   m = vmax( idata , 4 , sizeof(int) , intcmp ) ;
   printf( "%d" , idata[ m ] ) ;
   // strcmp関数を使って、sdata から最大値を探す
   m = vmax( sdata , 4 , sizeof(sdata[0]) , strcmp ) ;
   printf( "%s" , sdata[ m ] ) ;
}

このような、汎用化されたアルゴリズムを処理するプログラムに、関数ポインタを渡す方法では、qsort()が有名である。

純粋仮想基底クラスで書いてみる

class Object {
public:
   virtual int comp( Object* ) = 0 ;
   virtual void print() = 0 ;
} ;

int vmax( Object* array[] , int size ) {
   int max = 0 ;
   for( int i = 0 ; i < size ; i++ ) {
      if ( array[i]->comp( array[m] ) > 0 ) // 仮想関数で比較
         m = a ;
   }
   return m ;
}

class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) : data( x ) {}
   virtual void print() { printf( "%d" , data ) ; }
   virtual int comp( Object* p ) { // static_cast の説明は略
      return data - ((IntObject*)p)->data ;
   }
} ;

int main() {
   Object* array[] = {
      new IntObject( 10 ) , new IntObject( 5 ) , new IntObject( 20 )
   } ;
   array[ vmax( array , 3 ) ]->print() ; // 20
   return 0 ;
}

純粋仮想基底クラスを用いる場合は、純粋仮想基底クラスから、自分の処理のための型を派生させ、自分の型のための仮想関数を少し記述する…

しかし、このような純粋仮想基底クラスと仮想関数を使ったプログラミングは、メモリの使用効率・処理効率も高くないので、最近ではあまり使われない。

若干、オブジェクト指向とは関連が弱いが、テンプレートクラスを紹介する。

テンプレートクラス

C++のテンプレートクラスは、型の部分をテンプレート名として記述し、必要に応じて型を明示する。

// 足し算の処理を様々な型で記述するのは面倒
int add( int a , int b ) {
    return a + b ;
}
double add( double a , double b ) {
    return a + b ;
}
// templateを使って、必要に応じて関数を定義する。
template <typename T>
T add( T a , T b ) {
    return a + b ;
}
template <typename T>
int vmax( T a[] , int size ) {
   int m = 0 ;
   for( int i = 0 ; i 6lt; size ; i++ )
      if ( a[i] > a[m] )
         m = i ;
}
int ia[] = { 1 , 3 , 2 } ;
double fa[] = { 1.3 , 4.5 , 6.2 } ;
int main() {
    printf( "%d¥n" , add<int>( 3 , 4 ) ) ;
    printf( "%ld¥n" , add<double>( 3.0 , 4.0 ) ) ;
    printf( "%d¥n" , vmax<int>( ia , 3 ) ) ;
    printf( "%d¥n" , vmax<double>( faa , 3 ) ) ;
    return 0 ;
}

Googleスピードテスト

ブラウザを用いた Google によるインターネット速度テストの結果。

自宅、500Mbps超えてたのに比べたら…(x_x;

高専生の毒舌な妹bot より引用

移動平均のプログラム

移動平均のプログラム(ダサっ)

#include <stdio.h>
#define WIDTH 5
double xt[1000] ; // 元波形データ
double yt[1000] ; // 移動平均処理後のデータ
int main() {
    int    i ; 
    double t , x ;
    // 全データを読み込む(入力はコンマ区切りの2データ)
    for( i = 0 ; scanf( "%lf,%lf" , &t , &x ) == 2 ; i++ )
        xt[ i ] = x ;
    // データ xt[*] の移動平均を yt[*] に求める。
    for( i = WIDTH ; i < 1000 - WIDTH ; i++ ) {
        int j ;
        double s = 0.0 ; // 合計
        // 前後の値の合計を求める
        for( j = -WIDTH ; j <= WIDTH ; j++ )
            s = s + xt[ i + j ] ;
        // 合計をデータ数で割る
        yt[ i ] = s / (WIDTH * 2 + 1) ;
    }
    // 処理結果を出力する。
    for( i = 0 ; i < 1000 ; i++ )
        printf( "%d,%lf,%lf\n" , i , xt[i] , yt[i] ) ;
    return 0 ;
}

でも、このプログラムは、以下の点で問題がある。

  1. 範囲のデータを加算しているけど、加算の繰り返しが多い。
  2. 配列にデータを最初に全て読み込んでいるけど、長時間のデータならば大量のメモリが必要。
  3. 測定しながら移動平均を計算する場合、データはどうする?

移動平均のプログラム(ちょっと改良)

全部のデータを覚えるのはメモリの無駄なので、移動平均する直近のデータだけを覚えるように改良する。
しかし、データを保存する度に、配列をずらす処理も無駄なので、データを保存する場所(以下の例ではbp)を保存したら次の場所を示すように記述してみる。

#include <stdio.h>

#define WIDTH 10
double buff[ WIDTH ] ; // 直近のWIDTH個だけ保存
int    bp = 0 ;        // 最新データの場所
double bs = 0.0 ;      // 直近のWIDTH個の合計

int main() {
    int i ;
    double t , x ;
    for( i = 0 ; scanf( "%lf,%lf" , &t , &x ) == 2 ; i++ ) {
        // WIDTH個前のデータを捨てるために合計から引く
        bs = bs - buff[ bp ] ;

        buff[ bp ] = x ;      // 最新データを保存
        bs = bs + x ;         // 最新のデータで合計
        // 直近のデータを覚える場所を移動
        bp++ ;
        if ( bp >= WIDTH )
            bp = 0 ;
        // 移動平均を出力
        printf( "%d %lf\n" , i , bs / WIDTH ) ;
    }
    return 0 ;
}

仮想関数

仮想関数

前回の派生したプログラムで継承の説明をしたが、以下のようなプログラムでは、Student 型が混在した family[] の配列でも、Person へのポインタに「格下げ」されて保存されているため、
family[i]->print() では、Student 型でも Person型で表示処理が行われる。

class Student : public Person {
   :
   void print() {
      Person::print() ;                    // 名前と年齢を表示
      printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 53 ) ;
   saitoh.print() ; // t-saitoh 53

   Student mitsu( "mitsuki" , 18 , "E" , 4 ) ;
   Student ayuka( "ayuka" , 16 , "EI" , 2 ) ;
   mitsu.print() ; // mitsuki 18 / E 4   名前,年齢,所属,学年を表示
   ayuka.print() ; // ayuka 16 / EI 2    名前,年齢,所属,学年を表示

   Person* family[] = {
      &saitoh , &mitsu , &ayuka ,  // 配列の中に、Personへのポインタと
   } ;                             // Studentへのポインタが混在している
                                   // 派生クラスのポインタは、
                                   // 基底クラスのポインタとしても扱える
   for( int i = 0 ; i < 3 ; i++ )
      family[ i ]->print() ;       // t-saitoh 53/mitsuki 18/ayuka 16
}                                  //  が表示される。

しかし、Student型とPerson型の機能を活かせないだろうか?

仮想関数

このような場合、オブジェクト指向では、仮想関数の機能が便利である。

class Person {
   :
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
class Student : public Person {
   :
   virtual void print() {
      Person::print() ;                    // 名前と年齢を表示
      printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 53 ) ;
   saitoh.print() ; // t-saitoh 53

   Student mitsu( "mitsuki" , 18 , "E" , 4 ) ;
   Student ayuka( "ayuka" , 16 , "EI" , 2 ) ;
   mitsu.print() ; // mitsuki 18 / E 4   名前,年齢,所属,学年を表示
   ayuka.print() ; // ayuka 16 / EI 2    名前,年齢,所属,学年を表示

   Person* family[] = {
      &saitoh , &mitsu , &ayuka ,
   } ;
   for( int i = 0 ; i < 3 ; i++ )
      family[ i ]->print() ;       // t-saitoh 53/mitsuki 18,E 4/ayuka 16,EI 2
}

仮想関数が宣言されると、基底クラスの中に「型情報(PersonなのかStudentなのか)」が自動的に埋め込まれる。仮想関数を呼び出すと、型情報を使って、Person::print()を呼び出すか、Student::print()を呼び出すか、を選んでくれる。

仮想関数が生まれた背景

仮想関数は、GUI のプログラム記述に向いていた。例えば、GUIシステムでは、画面にデータを表示するための基本型として、座標と幅,高さの情報を持つ Window 型がある。

また、画面のアイコンは、Window型に、表示する絵の画像を追加した派生クラス WindowIcon 型、画面のテキストは、Windows型に、表示するテキストやフォント情報を持った、WindowText といった型のようなものとする。そうなると仮想関数の概念が無いと、display() を呼び出すためには、派生型の種類が大量ならばデータの型に応じた if 文を大量に書き並べないといけないし、データ型を区別するための情報(下記の例ならばtype)に、型毎に異なるマジックナンバーを埋め込む処理を自分で書かないといけない。

class Window {  // 概念を説明するための例にすぎない
private:
    int x , y , w , h ;
    int type ; // 型を区別するための情報
public:
    void display() {
        // 表示する処理
    }
} ;
class WindowIcon : public Window {
private:
    Image  img ;
public:
    void display() {
        // 画像を表示する処理
    }
} ;
class WindowText : public Window {
private:
    Font  font ;
    char* text ;
public:
    void display() {
        // テキストを表示する処理
    }
} ;
void main() {
    WindowIcon wi( アイコンのコンストラクタ ) ;
    WindowText wt( テキストのコンストラクタ ) ;
    Window* wins[] = {
        &wi , &wt , ...
    } ;
    for( int i = 0 ; i < 配列すべて ; i++ ) {
        if ( wins[i]->type が アイコンならば )
            wins[i]->display() ; // アイコンを表示する
        else if ( wins[ i ]->type が テキストならば )
            wins[i]->display() ; // テキストを表示する。
        else if ....
        :
    }
}

関数ポインタ

では、仮想関数はどのようなテクニックを用いて実装されているのだろうか?

これには、関数ポインタが用いられる。

int add( int x , int y ) {
    return x + y ;
}
int mul( int x , int y ) {
    return x * y ;
}
int main() {
    int (*func)( int , int ) ;
    func = add ; // add() ではない
    printf( "%d¥n" , (*func)( 3 , 4 ) ) ; // 7
    func = mul ;
    printf( "%d¥n" , (*func)( 3 , 4 ) ) ; // 12
    return 0 ;
}

仮想関数を用いると、基底クラスにはクラス毎の仮想関数への「関数ポインタ」などの型情報を保存する場所が自動的に作られ、基底クラス・派生クラスが使われると、そのオブジェクト毎に型情報を初期化する処理が行われる。仮想関数が呼び出されると、関数ポインタにより、各型毎のメソッドが呼び出されるため、大量の if 文は不要となる。

純粋仮想基底クラス

// 純粋仮想基底クラス
class Object {
public:
   virtual void print() = 0 ; // 中身の無い純粋基底クラスを記述しない時の書き方。
} ;
// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() {
      printf( "%d\n" , data ) ;
   }
} ;
// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() {
      printf( "%s\n" , data ) ;
   }
} ;
// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() {
      printf( "%lf\n" , data ) ;
   }
} ;
// 動作確認
int main() {
   Object* data[3] = {
      new IntObject( 123 ) ,
      new StringObject( "abc" ) ,
      new DoubleObject( 1.23 ) ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      data[i]->print() ;
   }
   return 0 ;
} ;

この書き方では、data[]には、整数、文字列、実数という異なるデータが入っているが、 Objectという純粋仮想基底クラスを通して、共通な型のように扱えるようになる。 そして、data[i]->print() では、各型の仮想関数が呼び出されるため、 「123 abc 1.23」 が表示される。

ここで、Object の様な一見すると中身が何もないクラスを宣言し、 このクラスから様々な派生クラスを用いるプログラムテクニックは、 広く利用され、Objectのような基底クラスは、純粋仮想基底クラスなどと呼ばれる。