Go(navigator)でブラウザ操作(テスト自動化など)

テストなどで、プログラムでブラウザを操作できないかなぁ・・・ というときに使うのが Selenium です。 で、これを Go で扱うためのパッケージとして、Agoutiがあります。

しかしながら、もうメンテナンスされていない模様。。。 ということで、navigatorを使ってみました。 下記は、このブログに対してゴニョゴニョやってみた例です。

package main

import (
    "context"
    "fmt"
    "github.com/ikawaha/navigator"
    "os"
    "time"
)

func main() {
    if err := run(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

func run() error {
    driver := navigator.ChromeDriver(navigator.Browser("chrome"), navigator.Debug)
    defer driver.Stop()

    ctx := context.Background()
    if err := driver.Start(ctx); err != nil {
        return fmt.Errorf("driver.Start() failed: %w", err)
    }

    page, err := driver.NewPage()
    if err != nil {
        return fmt.Errorf("driver.NewPage() failed: %w", err)
    }

    // ブログトップを開く
    page.Navigate("https://yumgoo17.hatenablog.com/")

    // 検索ボックスに TensorFlow と入力
    if err := page.FindByName("q").Fill("TensorFlow"); err != nil {
        return err
    }

    // 検索実行(サブミット)
    if err := page.FindByClass("search-module-button").Submit(); err != nil {
        return err
    }

    // あるブログ記事を開く
    page.Navigate("https://yumgoo17.hatenablog.com/entry/2023/05/10/Go_%E3%81%AE%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%83%BB%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E5%91%A8%E3%82%8A%E8%A6%9A%E6%9B%B8%E3%81%8D")

    // 「コメントを書く」をクリック
    if err := page.FindByClass("leave-comment-title").Click(); err != nil {
        return err
    }

    // 新しく開いたウィンドウにフォーカスする
    if err := page.NextWindow(); err != nil {
        return err
    }

    // 表示されるまでちょっと待つ
    time.Sleep(1 * time.Second)

    // 投稿者・URL・メール・コメントを入力
    if err := page.FindByID("name").Fill("テスト太郎"); err != nil {
        return err
    }
    if err := page.FindByID("url").Fill("https://test.example.test.com"); err != nil {
        return err
    }
    if err := page.FindByID("mail").Fill("test@test.example.test.com"); err != nil {
        return err
    }
    if err := page.FindByID("body").Fill("テストコメント"); err != nil {
        return err
    }

    // reCAPTCHA は対応できず

    // Submit
    //if err := page.FindByID("submit").Submit(); err != nil {
    // return err
    //}

    time.Sleep(2 * time.Second)

    return nil
}

上記ソースや、navigatorのREADMEを見てもらってもわかると思いますが、とてもわかりやすく簡単に使えますね。

このブログにコメントしようとすると、ログインしてないときは reCAPTCHA が必要なようですが、当然のことながら、それは突破できませんでした。

Go のメソッド・インタフェース周り覚書き

Go にもだいぶ慣れてきたが、インタフェース周りがあやふやになる。 なのでひと目で理解できるように、ソースコードで覚書き(自分用)

package main

import "fmt"

type Profile struct {
    Name   string
    Age    int
    Height int
    Weight int
}

type Dog struct {
    Profile
}

type Person struct {
    Profile
}

type Animal interface {
    Eat()
    Run()
    Walk()
    Sleep()
}

func (dog Dog) Eat() {
    fmt.Println("mog-mog")
}

func (dog Dog) Run() {
    fmt.Println("Dash!!")
}

func (dog Dog) Walk() {
    fmt.Println("Peta-Peta")
}

func (dog Dog) Sleep() {
    fmt.Println("Zzz")
}

func (dog Dog) Bark() {
    fmt.Println("Wan-Wan!")
}

func (person Person) Eat() {
    fmt.Println("mog-mog")
}

func (person Person) Run() {
    fmt.Println("Dash!!")
}

func (person Person) Walk() {
    fmt.Println("Peta-Peta")
}

func (person Person) Sleep() {
    fmt.Println("Zzz")
}

func (person Person) Greet() {
    fmt.Println("Hello!!")
}

func RunAndWalk(animal Animal) {
    animal.Run()
    animal.Walk()
}

func EatAndSleep(animal Animal) {
    animal.Eat()
    animal.Sleep()
}

func NewDog(name string, age int, height int, weight int) Dog {
    return Dog{
        Profile{
            Name:   name,
            Age:    age,
            Height: height,
            Weight: weight,
        },
    }
}

func NewPerson(name string, age int, height int, weight int) Person {
    return Person{
        Profile{
            Name:   name,
            Age:    age,
            Height: height,
            Weight: weight,
        },
    }
}

func main() {
    pochi := NewDog("Pochi", 5, 70, 20)
    akira := NewPerson("Akira", 17, 171, 68)

    RunAndWalk(pochi)
    RunAndWalk(akira)
    EatAndSleep(pochi)
    EatAndSleep(akira)

    pochi.Bark()
    akira.Greet()
}

React でお手軽にタイピングゲームもどきを作る(改善編)

前回、React でタイピングゲームもどきを作りましたが、それを改善していきたいと思います。開発環境は前回と変わりません。

改善する箇所

前回作ったものではタイプミスをしても何も起きませんでしたが、今回は、タイプミスをしたときはエラーとし、間違っている旨を表示、正しい文字を入力するまで次の文字を入力できないようにします。動作デモをご覧ください。

ソースコード

今回は状況の文字列に色を付けたいので、少しだけ css を使います。新しく src/typing.css ファイルを作り、そこに css を記述しました。中身は下記の通りで、文字色の指定をしているだけです。

.status-normal {
    color: black;
}

.status-error {
    color: red;
}

.status-correct {
    color: blue;
}

続いて src/index.js のソースを全て貼ります。

import React from "react";
import ReactDOM from "react-dom/client";
import './typing.css';

class Typing extends React.Component {
  /**
   * コンストラクタ
   * @param props
   */
  constructor(props) {
    super(props);
    // お題リスト
    this.subjectList = ['apple', 'orange', 'banana'];
    this.state = {
      subjectIndex: 0,                // 現在のお題番号
      inputValue: '',                 // 入力欄テキストボックスの値
      subject: this.subjectList[0],   // お題
      status: {                       // 現在のステータス
        message: '間違ってます。',
        css: 'status-normal',
      },
    }
  }

  /**
   * ChangeEvent を取り扱う
   * @param {React.ChangeEvent<HTMLInputElement>} e イベントオブジェクト
   */
  handleChange(e) {
    // お題が終わっていたら終了
    if (this.isFinished(this.state.subjectIndex)) {
      return;
    }

    const stringIndex = e.target.value.length - 1;
    const inputCharacter = e.target.value[stringIndex];
    const subjectCharacter = this.state.subject[stringIndex];

    if (inputCharacter !== subjectCharacter) {
      // タイプミスのとき
      this.setStatus('error');
      setTimeout(
        () => {this.setStatus('normal')},
        1000
      )
    } else {
      // OK
      this.setState({inputValue: e.target.value});
    }

    // 正解したとき
    if (e.target.value === this.state.subject) {
      this.setStatus('correct');
      setTimeout(() => {this.setNextSubject()}, 1000);
    }
  }

  /**
   * 次のお題を設定する
   */
  setNextSubject() {
    const nextSubjectIndex = this.state.subjectIndex + 1;

    // お題が終わっていたら終了
    if (this.isFinished(nextSubjectIndex)) {
      this.setState({
        subjectIndex: nextSubjectIndex,
        inputValue: '',
        subject: '',
      });
      this.setStatus('finish');
      return;
    }

    this.setState({
      subjectIndex: nextSubjectIndex,
      inputValue: '',
      subject: this.subjectList[nextSubjectIndex],
    });
    this.setStatus('normal');
  }

  /**
   * this.state.status を設定する
   * @param kind 設定種別
   */
  setStatus(kind = 'normal') {
    // コンストラクタで定義しておいた方が良いかも
    const statusMap = {
      'normal': {message: '入力してください。', css: 'status-normal'},
      'error': {message: '違います!', css: 'status-error'},
      'correct': {message: '正解!', css: 'status-correct'},
      'finish': {message: '終わり。', css: 'status-normal'},
    };
    this.setState({status: statusMap[kind]});
  }

  /**
   * お題が終了しているかどうかを返す
   * @param subjectIndex 現在のお題番号
   * @returns {boolean}
   */
  isFinished(subjectIndex) {
    return subjectIndex >= this.subjectList.length;
  }

  /**
   * 描画する
   * @returns {JSX.Element}
   */
  render() {
    return (
      <div>
        <p>お題:{this.state.subject}</p>
        <p>状況:<span className={this.state.status.css}>{this.state.status.message}</span></p>

        入力してください。<br></br>
        <input
          type="text"
          onChange={(e) => this.handleChange(e)}
          value={this.state.inputValue}
        />
      </div>
    );
  }
}

// ========================================
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Typing/>);

変更点を中心に一つ一つ解説します。

class Typing extends React.Component {
  /**
   * コンストラクタ
   * @param props
   */
  constructor(props) {
    super(props);
    // お題リスト
    this.subjectList = ['apple', 'orange', 'banana'];
    this.state = {
      subjectIndex: 0,                // 現在のお題番号
      inputValue: '',                 // 入力欄テキストボックスの値
      subject: this.subjectList[0],   // お題
      status: {                       // 現在のステータス
        message: '間違ってます。',
        css: 'status-normal',
      },
    }
  }

コンストラクタです。this.state ですが、現在のステータス「状況」を表す this.state.status について messagecss の2つの要素に分けます。これは現在の状況に合わせて、文字色を変えたいからです。

  /**
   * 描画する
   * @returns {JSX.Element}
   */
  render() {
    return (
      <div>
        <p>お題:{this.state.subject}</p>
        <p>状況:<span className={this.state.status.css}>{this.state.status.message}</span></p>

        入力してください。<br></br>
        <input
          type="text"
          onChange={(e) => this.handleChange(e)}
          value={this.state.inputValue}
        />
      </div>
    );
  }

順番が前後しますが、render() を解説します。「状況」を表示する部分を <span> で囲い、className 属性を持たせました。これで、クラスを動的に変えることで、文字色を動的に変えることができるようになりました。this.state.statusmessagecss の2要素に分けたので、もちろんその対応もしています。

  /**
   * this.state.status を設定する
   * @param kind 設定種別
   */
  setStatus(kind = 'normal') {
    // コンストラクタで定義しておいた方が良いかも
    const statusMap = {
      'normal': {message: '入力してください。', css: 'status-normal'},
      'error': {message: '違います!', css: 'status-error'},
      'correct': {message: '正解!', css: 'status-correct'},
      'finish': {message: '終わり。', css: 'status-normal'},
    };
    this.setState({status: statusMap[kind]});
  }

また、順番が前後しますが、先に setStatus() を解説します。名前の通り、this.state.status を設定するメソッドです。引数 kind が取りうる値は normal(通常), error(エラー), correct(正解), finish(終了) の4つで、statusMap にその状況(kind) のときに this.state.status に設定する値を定義してあります。例えば、タイプミスをしたときは、引数に kind = 'error' が渡ってきて、statusMap から error のときの値が取得されます。文言 message は「違います!」で、css(クラス)は status-error つまり文字色を赤に設定しているクラスです。これを this.setState()this.state.status に設定します。これで「状況」の所に赤い文字で「違います!」と表示されます。

  /**
   * お題が終了しているかどうかを返す
   * @param subjectIndex 現在のお題番号
   * @returns {boolean}
   */
  isFinished(subjectIndex) {
    return subjectIndex >= this.subjectList.length;
  }

続いて、isFinished() ですが、お題が終了しているかどうかを返すメソッドです。引数 subjectIndex(現在のお題番号)と、お題リスト this.subjectList の要素数を比較した結果を返しています。1行だけの処理なので、メソッド化する必要も無さそうですが、isFinished() というメソッド名から、何をしているメソッドか一目でわかるため、使い勝手が良く、メソッドに切り出しておくと何かと便利です。

  /**
   * ChangeEvent を取り扱う
   * @param {React.ChangeEvent<HTMLInputElement>} e イベントオブジェクト
   */
  handleChange(e) {
    // お題が終わっていたら終了
    if (this.isFinished(this.state.subjectIndex)) {
      return;
    }

    const stringIndex = e.target.value.length - 1;
    const inputCharacter = e.target.value[stringIndex];
    const subjectCharacter = this.state.subject[stringIndex];

    if (inputCharacter !== subjectCharacter) {
      // タイプミスのとき
      this.setStatus('error');
      setTimeout(
        () => {this.setStatus('normal')},
        1000
      );
    } else {
      // OK
      this.setState({inputValue: e.target.value});
    }

    // 正解したとき
    if (e.target.value === this.state.subject) {
      this.setStatus('correct');
      setTimeout(() => {this.setNextSubject()}, 1000);
    }
  }

テキストボックスに文字が入力される度に呼ばれる this.handleChange() です。まず始めに、既にお題が終わっているかどうかを判定し、終わっていれば何もせず終了します。

今回はタイプミスをしたらエラーにするので、入力された一文字一文字ごとに正誤判定をする必要があります。文字列は配列のようにインデックスを指定して、一文字を参照することができます。今、入力している文字列の入力している位置は、一番右なので e.target.value.length - 1 で取得することができます。この取得した位置を用いて、今、入力した文字 inputCharacter と、お題に対する「取得した位置」にある文字 subjectCharacter の両者を比較して正誤判定をします。

タイプミスをしていれば、this.setStatus('error') で「状況」をエラーの状態にし、1秒後に通常の状態に戻します。タイプミスをしていないときは、this.setState() で、入力された文字列をテキストボックスに反映します。そして、お題に正解したときは、this.setStatus('correct') で「状況」を正解の状態にして、1秒後に次のお題を出力するメソッド this.setNextSubject() を呼び出します。

  /**
   * 次のお題を設定する
   */
  setNextSubject() {
    const nextSubjectIndex = this.state.subjectIndex + 1;

    // お題が終わっていたら終了
    if (this.isFinished(nextSubjectIndex)) {
      this.setState({
        subjectIndex: nextSubjectIndex,
        inputValue: '',
        subject: '',
      });
      this.setStatus('finish');
      return;
    }

    this.setState({
      subjectIndex: nextSubjectIndex,
      inputValue: '',
      subject: this.subjectList[nextSubjectIndex],
    });
    this.setStatus('normal');
  }

次のお題を設定するメソッドです。まず、現在のお題番号に1を足して、次のお題番号を取得します。そして、お題が終わっているかどうかを判定して、終わっていたら this.setState() で、お題番号 subjectIndex を次のお題番号に、テキストボックスの中身 inputValue を空に、お題 subject も空にそれぞれ設定します。そして、this.setStatus('finish') を呼んで、「状況」を終了の状態にします。this.setState() で、「状況」this.state.status も設定できるのですが、this.setStatus() を使った方が便利で間違いがありません。

お題が終わっていないときは、お題番号 subjectIndex を次のお題番号に、テキストボックスの中身 inputValue を空に、お題リスト this.subjectList から次のお題を取得して、お題 subject を次のお題にそれぞれ設定します。そして this.setStatus('normal') を呼んで、「状況」を通常の状態にします。

今回はタイピングゲームもどきを改善しました。次に修正するとすれば、ソースが長くなってきたので、コンポーネントを分けた方がよいかもしれません。

React でお手軽にタイピングゲームもどきを作る

前々から React の勉強をしたいと思っていたのですが、勉強がてら、とても簡単なタイピングゲームもどきを作ってみたので紹介します。

作ったもの

デモを用意したのでご覧ください。

開発環境

Docker で開発環境を構築しました。参考にしたサイトは記事の最後に載せておきますので参考にしてください。ライブラリのバージョンは下記の通りです。

  • node:19.0.0
  • react:18.2.0

最初に下記コマンドを実行後 src ディレクトリ配下のファイルは全て削除しています。

npx create-react-app my-app

プログラムソース

src/index.js を作成し、そこにコードを作成していきます。小規模なので複数のコンポーネントに分けたりはしていません。まずは、src/index.js の全てを貼ります。

import React from "react";
import ReactDOM from "react-dom/client";

class Typing extends React.Component {
  /**
   * コンストラクタ
   * @param props
   */
  constructor(props) {
    super(props);
    // お題リスト
    this.subjectList = ['apple', 'orange', 'banana'];
    this.state = {
      subjectIndex: 0,                // 現在のお題番号
      inputValue: '',                 // 入力欄テキストボックスの値
      subject: this.subjectList[0],   // お題
      status: '間違ってます。',         // 現在のステータス
    }
  }

  /**
   * ChangeEvent を取り扱う
   * @param {React.ChangeEvent<HTMLInputElement>} e イベントオブジェクト
   */
  handleChange(e) {
    this.setState({inputValue: e.target.value});

    if (e.target.value === this.state.subject) {
      // 正解したら
      this.setState({status: '正解!'});
      setTimeout(() => {this.setNextSubject()}, 1000);
    }
  }

  /**
   * 次のお題を設定する
   */
  setNextSubject() {
    let nextSubjectIndex = this.state.subjectIndex + 1;
    if (nextSubjectIndex >= this.subjectList.length) {
      this.setState({status: '終わり。'});
      return;
    }

    this.setState({
      subjectIndex: nextSubjectIndex,
      inputValue: '',
      subject: this.subjectList[nextSubjectIndex],
      status: '間違ってます。'
    });
  }

  /**
   * 描画する
   * @returns {JSX.Element}
   */
  render() {
    return (
      <div>
        <p>お題:{this.state.subject}</p>
        <p>状況:{this.state.status}</p>

        入力してください。<br></br>
        <input
          type="text"
          onChange={(e) => this.handleChange(e)}
          value={this.state.inputValue}
        />
      </div>
    );
  }
}

// ========================================
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Typing/>);

一つ一つ解説します。

import React from "react";
import ReactDOM from "react-dom/client";

class Typing extends React.Component {
  /**
   * コンストラクタ
   * @param props
   */
  constructor(props) {
    super(props);
    // お題リスト
    this.subjectList = ['apple', 'orange', 'banana'];
    this.state = {
      subjectIndex: 0,                // 現在のお題番号
      inputValue: '',                 // 入力欄テキストボックスの値
      subject: this.subjectList[0],   // お題
      status: '間違ってます。',         // 現在のステータス
    }
  }

まず一番始めに必要なものをインポートします。今回は複数のコンポーネントに分けていないので、クラスは Typing クラス1つだけになります。コンストラクタで設定している this.state ですが、これはコンポーネントの情報(状態)を持つものです。this.state の値を更新すると、その値を利用している箇所を変更(再描画)させることができます。今回 this.state には制御したいもの、つまり、テキストボックスに文字が入力されたら変更(再描画)させたいものを設定しています。例えば、お題通りの文字が入力され正解になったら、次のお題を出題するわけですが、お題番号やお題を書き換えるためにその値をここで管理しています。

  /**
   * 描画する
   * @returns {JSX.Element}
   */
  render() {
    return (
      <div>
        <p>お題:{this.state.subject}</p>
        <p>状況:{this.state.status}</p>

        入力してください。<br></br>
        <input
          type="text"
          onChange={(e) => this.handleChange(e)}
          value={this.state.inputValue}
        />
      </div>
    );
  }

順番が前後しますが、先に render() を解説します。ここで return しているものが画面に表示されています。お題と状況は this.state に設定したものを表示させています。テキストボックスですが、onChange={(e) => this.handleChange(e)} で、文字が入力される度に this.handleChange() が呼ばれるようにしています。value つまりテキストボックスの中身は this.state.inputValue で管理します。これはお題に正解して、次のお題に進む時に、テキストボックスの中身を空にするために利用します。

  /**
   * ChangeEvent を取り扱う
   * @param {React.ChangeEvent<HTMLInputElement>} e イベントオブジェクト
   */
  handleChange(e) {
    this.setState({inputValue: e.target.value});

    if (e.target.value === this.state.subject) {
      // 正解したら
      this.setState({status: '正解!'});
      setTimeout(() => {this.setNextSubject()}, 1000);
    }
  }

テキストボックスに文字が入力される度に呼ばれるメソッドです。まず始めの行ですが、this.setState()this.state.inputValue つまり、テキストボックスの中身を設定しています。e.target.value はテキストボックスに入力された文字列なので、つまり、テキストボックスに入力された値を、そのままテキストボックスに反映させています。テキストボックスの中身を管理しているのは、正解時にテキストボックスを空にしたいから、という理由だけなので、正解時以外は普通に動作するようにします。

そして if 文の中が正解したときの処理になります。正解したら this.state.status を「正解!」に変えて、1秒後に次のお題を出すメソッド this.setNextSubject() を呼びます。なぜ1秒後にしているかというと、すぐに this.setNextSubject() を呼び出すと、その中ですぐに this.state.status が「間違ってます。」に設定されてしまうので、見た目的に「正解!」に変わったように見えないためです。また setTimeout(this.setNextSubject, 1000) と書いても良さそうな気がしますが、そうすると this.setNextSubject() 内の thisundefined 等になってしまうので、アロー関数を渡しています。

  /**
   * 次のお題を設定する
   */
  setNextSubject() {
    let nextSubjectIndex = this.state.subjectIndex + 1;
    if (nextSubjectIndex >= this.subjectList.length) {
      this.setState({status: '終わり。'});
      return;
    }

    this.setState({
      subjectIndex: nextSubjectIndex,
      inputValue: '',
      subject: this.subjectList[nextSubjectIndex],
      status: '間違ってます。'
    });
  }

次のお題を設定するメソッドです。まずお題番号をインクリメントします。お題番号がお題数以上であれば、this.state.status を「終わり。」に変えて処理を終了します。お題番号がお題数未満であれば、お題番号、お題を次のお題に設定し、this.state.status を「間違ってます。」に戻し、this.state.inputValue を空にすることで、テキストボックスの中身を空にします。

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Typing/>);

最後に作ったものを描画して完成です。

今回の作ったタイピングゲームでは、タイプミスしても、特にペナルティがありません。よくあるタイピングゲームだと、タイプミスをしたらエラーになり、正しく入力するまで次の文字に進めない、という動作をすることが多いと思いますので、次回はその機能を実装していきたいと思います。

参考サイト

TensorFlow・Keras を使って分類問題を学習する

今回は前回に引き続き、TensorFlow・Keras を用いて分類問題を学習していきたいと思います。設定や学習する問題は、前回の分類問題とできるだけ同じになるようにします。

学習する問題

前回の分類問題と同じ問題です。xy平面上に等間隔に400個の点を配置し、xy平面をサインカーブ2つの領域に区切ります。400個の点がどちらの領域に属しているのかを学習します。前回の分類問題で詳しく説明していますので、参照してください。

設定

入力層・中間層・出力層の3層からなるネットワークにします。各種設定は下記の通りです。

環境

前回の記事で回帰問題をkerasで学習したときと同じく TensorFlow の Docker イメージ tensorflow/tensorflow:latest-jupyter でコンテナを立ち上げ、jupyter notebook 上で動作させています。

プログラムソース

今回もまずは全ソースを貼ります。

import os
import shutil
import random

import matplotlib.pyplot as plt
import numpy as np
from keras.models import Sequential, load_model
from keras.layers import Dense
from keras.callbacks import ModelCheckpoint
%matplotlib inline

def create_result_distribution_chart(inputs_list, outputs_list):
    """結果の散布図を作成する

    :param inputs_list: 入力値のリスト
    :param outputs_list: 出力値のリスト
    :param path: 散布図を出力するファイルパス
    """
    x1, y1, x2, y2 = [], [], [], []

    for inputs, outputs in zip(inputs_list, outputs_list):
        _x, _y = inputs
        # どちらに判定されているかで分類する
        if outputs[0] >= outputs[1]:
            x1.append(_x)
            y1.append(_y)
        else:
            x2.append(_x)
            y2.append(_y)

    plt.scatter(x1, y1, s=20, c="blue", marker="*")
    plt.scatter(x2, y2, s=20, c="green")

    # 境界となる線を書く
    pi = 3.141592  # 円周率
    x = np.linspace(-pi, pi, 100)
    y = [target_func(_x) for _x in x]
    plt.plot(x, y, color="orange")

# 設定
split_num = 20      # np.linspace で何分割にするか
neuron_num = 20     # 中間層のニューロン数
batch_size = 1      # バッチサイズ
epoch = 250         # エポック数

target_func = lambda x: np.sin(x)       # 境界線となる関数(サインカーブ)
model_dir = "/root/practice/model/"     # 保存したモデルファイルを格納するディレクトリ

# ディレクトリを空にする
shutil.rmtree(model_dir)
os.makedirs(model_dir, exist_ok=True)

# 訓練・教師データ作成
trainings, teachings = [], []

x = np.linspace(-3, 3, split_num)
y = np.linspace(-1, 1, split_num)
random.shuffle(x)
random.shuffle(y)

for _x in x:
    value = target_func(_x)

    for _y in y:
        teaching = [1, 0] if _y >= value else [0, 1]
        trainings.append([_x, _y])
        teachings.append(teaching)

# モデル作成
model = Sequential()
model.add(Dense(neuron_num, activation='sigmoid', input_shape=(2,)))
model.add(Dense(2, activation='softmax'))
model.compile(optimizer='SGD', loss='categorical_crossentropy')

checkpoint = ModelCheckpoint(os.path.join(model_dir, "{epoch:03d}.hdf5"), verbose=0)

# 学習
history = model.fit(
    trainings,
    teachings,
    batch_size=batch_size,
    epochs=epoch,
    verbose=1,
    callbacks=[checkpoint]
)

# 結果表示
plt.plot(history.history["loss"])

# 予測
predicts = model.predict(trainings, 1, verbose=1)
create_result_distribution_chart(trainings, predicts)

一つ一つ説明します。

import os
import shutil
import random

import matplotlib.pyplot as plt
import numpy as np
from keras.models import Sequential, load_model
from keras.layers import Dense
from keras.callbacks import ModelCheckpoint
%matplotlib inline

必要なライブラリのインポートと、matprotlib のグラフをインラインで表示させる記述です。

def create_result_distribution_chart(inputs_list, outputs_list):
    """結果の散布図を作成する

    :param inputs_list: 入力値のリスト
    :param outputs_list: 出力値のリスト
    :param path: 散布図を出力するファイルパス
    """
    x1, y1, x2, y2 = [], [], [], []

    for inputs, outputs in zip(inputs_list, outputs_list):
        _x, _y = inputs
        # どちらに判定されているかで分類する
        if outputs[0] >= outputs[1]:
            x1.append(_x)
            y1.append(_y)
        else:
            x2.append(_x)
            y2.append(_y)

    plt.scatter(x1, y1, s=20, c="blue", marker="*")
    plt.scatter(x2, y2, s=20, c="green")

    # 境界となる線を書く
    pi = 3.141592  # 円周率
    x = np.linspace(-pi, pi, 100)
    y = [target_func(_x) for _x in x]
    plt.plot(x, y, color="orange")

まずは、予測結果を散布図で描画する関数を定義します。前回、分類問題を学習したときに定義した関数と同じ関数なので、詳細は前回、分類問題を学習したときの記事を参照してください。引数 inputs_list に訓練データを、outputs_listニューラルネットワークの出力を渡せば、学習結果を表示させることができます。

# 設定
split_num = 20      # numpy.linspace で何分割にするか
neuron_num = 20     # 中間層のニューロン数
batch_size = 1      # バッチサイズ
epoch = 250         # エポック数
target_func = lambda x: np.sin(x)       # 境界線となる関数(サインカーブ)
model_dir = "/root/practice/model/"     # 保存したモデルファイルを格納するディレクトリ

# ディレクトリを空にする
shutil.rmtree(model_dir)
os.makedirs(model_dir, exist_ok=True)

各種設定です。前回の分類問題となるべく同じになるように設定します。numpy.linspace による分割数を20(訓練データ数は20×20=40000)、中間層のニューロン数を20、オンライン学習なのでバッチサイズを1、エポック数を250とします。(総学習回数は訓練データ数40000 × エポック数250 = 100万回となる)

# 訓練・教師データ作成
trainings, teachings = [], []

x = np.linspace(-3, 3, split_num)
y = np.linspace(-1, 1, split_num)
random.shuffle(x)
random.shuffle(y)

for _x in x:
    value = target_func(_x)

    for _y in y:
        teaching = [1, 0] if _y >= value else [0, 1]
        trainings.append([_x, _y])
        teachings.append(teaching)

訓練・教師データを生成します。作り方は、前回分類問題を学習したときと同じです。

# モデル作成
model = Sequential()
model.add(Dense(neuron_num, activation='sigmoid', input_shape=(2,)))    // (1)
model.add(Dense(2, activation='softmax'))   // (2)
model.compile(optimizer='SGD', loss='categorical_crossentropy')   // (3)

モデルを作成します。(1)の中間層・入力層ですが、中間層のニューロン数は neuron_num なので20、活性化関数 activationシグモイド関数を設定します。入力の形状 input_shape は x座標とy座標の2つなので、(2,) となります。(2) の出力層は、出力は2つなのでニューロン数は2、活性化関数 activation はソフトマックス関数を設定します。(3) の model の設定では、最適化アルゴリズム optimizer確率的勾配降下法(Stochastic Gradient Descent)を、損失関数 loss にクロスエントロピー誤差を設定します。

checkpoint = ModelCheckpoint(os.path.join(model_dir, "{epoch:03d}.hdf5"), verbose=0)

# 学習
history = model.fit(
    trainings,
    teachings,
    batch_size=batch_size,
    epochs=epoch,
    verbose=1,
    callbacks=[checkpoint]
)

fit メソッドで学習を実行します。引数に、訓練・教師データやバッチサイズなど、もろもろを渡して実行します。

# 結果表示
plt.plot(history.history["loss"])

# 予測
predicts = model.predict(trainings, 1, verbose=1)
create_result_distribution_chart(trainings, predicts)

誤差の推移をグラフで表示し、学習後のニューラルネットワークを利用して、入力座標がサインカーブの上か下かのどちらに属しているかを予測をします。その結果を、冒頭で定義したcreate_result_distribution_chart() を利用して散布図で表示します。

結果

結果は下記の通りになりました。まずは誤差の推移です。    縦軸が誤差、横軸がエポックです。順調に誤差が減少していくのがわかります。

続いて、学習後のニューラルネットワークで予測した結果です。   x座標の端の方、-3や3のあたりは完全に分類できていませんが、ほとんどの点(座標)で正しく分類できていることがわかります。

学習途中のニューラルネットワークで予測した結果も表示します。下記のソースで散布図を表示します。

# 途中結果表示用
target_model = load_model(os.path.join(model_dir, "001.hdf5"))
target_predicts = target_model.predict(trainings, 1, verbose=1)
create_result_distribution_chart(trainings, target_predicts)

上から順に1エポック終了後、100エポック終了後、200エポック終了後のニューラルネットワークが予測した結果になります。

1エポック終了後では、全然学習が足りず、多くの点(座標)でうまく分類できていません。それから学習が進むにつれて、点(座標)がうまく分類できていくことがわかります。200エポック終了後だと、250エポック終了後とほぼ変わらない結果が出ていることがわかります。

TensorFlow・Keras を使って回帰問題を学習する

前回まで学習してきた、回帰問題と分類問題を TensorFlow・Keras を用いて学習していきたいと思います。 まずは、回帰問題から学習します。設定や学習する問題は、前回の回帰問題とできるだけ同じになるようにします。

学習する問題

前回の回帰問題と同じく y = sin(x) という、いわゆるサインカーブを学習します。入力 x に対して、出力 y の値が sin(x) に近いほど良いということになります。

設定

入力層・中間層・出力層の3層からなるネットワークにします。各種設定は下記の通りです。

  • 活性化関数(中間層):シグモイド関数
  • 活性化関数(出力層):恒等関数
  • 誤差:二乗和誤差
  • 学習:オンライン学習

環境

TensorFlow の Docker イメージ tensorflow/tensorflow:latest-jupyter でコンテナを立ち上げ、jupyter notebook 上で動作させています。そのため、下記に紹介するソースの中で、matplotlib によるグラフ表示は、jupyter notebook でないと適切に表示されないと思われるので、ご注意ください。

プログラムソース

まずは、ソースの全てを貼ります。

import copy
import os
import shutil

import matplotlib.pyplot as plt
import numpy as np
from keras.callbacks import ModelCheckpoint
from keras.layers import Dense
from keras.models import Sequential, load_model
%matplotlib inline

def prot_result(ordered_x, ordered_y, predicts):
    """予測結果を描画する

    :param ordered_x: 順番に並んだx座標のデータ
    :param ordered_y: 順番に並んだy座標のデータ
    :param predicts: ニューラルネットワークで計算されたy座標のデータ
    """
    plt.plot(ordered_x, ordered_y, color="green", label="sin(x)")
    plt.plot(ordered_x, predicts, color="blue", linestyle="--", label="predicts")
    plt.legend()


# 設定
train_num = 1000    # 訓練データ数
batch_size = 1      # バッチサイズ
epochs = 20         # エポック数
neuron_num = 15     # 中間層のニューロン数
model_dir = "/root/practice/model/"

# ディレクトリを空にする
shutil.rmtree(model_dir)
os.makedirs(model_dir, exist_ok=True)

# 訓練・教師データ作成
ordered_x = np.linspace(-np.pi, np.pi, train_num)
ordered_y = np.sin(ordered_x)

trainings = copy.deepcopy(ordered_x)
np.random.shuffle(trainings)
teachings = np.sin(trainings)

# モデル作成
model = Sequential()
model.add(Dense(neuron_num, activation="sigmoid", input_shape=(1,)))
model.add(Dense(1))
model.compile(optimizer="SGD", loss="mse")

model_file = os.path.join(model_dir, "{epoch:03d}.hdf5")
checkpoint = ModelCheckpoint(model_file, verbose=0)

# 学習
history = model.fit(
    trainings,
    teachings,
    batch_size=batch_size,
    epochs=epochs,
    verbose=1,
    callbacks=[checkpoint],
)

# 結果表示
plt.plot(history.history["loss"])

predicts = model.predict(ordered_x, 1, verbose=1)
prot_result(ordered_x, ordered_y, predicts)

一つ一つ説明します。

import copy
import os
import shutil

import matplotlib.pyplot as plt
import numpy as np
from keras.callbacks import ModelCheckpoint
from keras.layers import Dense
from keras.models import Sequential, load_model
%matplotlib inline

必要なライブラリのインポートと、matplotlib のグラフをインラインで表示させるための記述です。

def prot_result(ordered_x, ordered_y, predicts):
    """予測結果を描画する

    :param ordered_x: 順番に並んだx座標のデータ
    :param ordered_y: 順番に並んだy座標のデータ
    :param predicts: ニューラルネットワークで計算されたy座標のデータ
    """
    plt.plot(ordered_x, ordered_y, color="green", label="sin(x)")
    plt.plot(ordered_x, predicts, color="blue", linestyle="--", label="predicts")
    plt.legend()

予測結果をグラフで描画する関数を先に定義しておきます。引数 predicts は構築したニューラルネットワークで予測されたy座標のデータになります。正解のサインカーブと合わせて描画します。

# 設定
train_num = 1000    # 訓練データ数
batch_size = 1      # バッチサイズ
epochs = 20         # エポック数
neuron_num = 15     # 中間層のニューロン数
model_dir = "/root/practice/model/" # 保存したモデルファイルを格納するディレクトリ

# ディレクトリを空にする
shutil.rmtree(model_dir)
os.makedirs(model_dir, exist_ok=True)

各種設定です。前回の回帰問題と、なるべく同じになるよう設定します。訓練データ数を1000、オンライン学習なのでバッチサイズを1、中間層のニューロン数を15とします。総学習回数を同じにするためにエポック数は20にします。(学習回数は訓練データ数×エポック数で20000回となる)モデルファイルを保存するディレクトリを設定し、中身を空にしておきます。

# 訓練・教師データ作成
ordered_x = np.linspace(-np.pi, np.pi, train_num)
ordered_y = np.sin(ordered_x)

trainings = copy.deepcopy(ordered_x)
np.random.shuffle(trainings)
teachings = np.sin(trainings)

訓練・教師データを作成します。作り方は前回の回帰問題と同じです。今回は numpy を使っているので、前回よりもかなり簡素に書けています。

# モデル作成
model = Sequential()
model.add(Dense(neuron_num, activation="sigmoid", input_shape=(1,)))    // (1)
model.add(Dense(1))     // (2)
model.compile(optimizer="SGD", loss="mse")  // (3)

Sequential を利用してモデルを生成し、add メソッドで層を追加していきます。 まず(1)は中間層と入力層に当たる部分です。Dense は全結合の層で、第一引数はニューロン数、引数 activation は活性化関数、input_shape は入力の形状を表します。今回はx座標の1つしか入力がないので、(1,) となります。(2) は出力層に当たる部分です。出力はy座標の一つしか無いので、第一引数のニューロン数は1、活性化関数は恒等関数なので特に指定していません。(3) の compile メソッドでモデルの設定をします。引数 optimizer は最適化アルゴリズムで、確率的勾配降下法(Stochastic Gradient Descent)を設定します。loss は損失関数で、平均二乗誤差(Mean Squared Error)を設定します。

model_file = os.path.join(model_dir, "{epoch:03d}.hdf5")
checkpoint = ModelCheckpoint(model_file, verbose=0)

# 学習
history = model.fit(
    trainings,
    teachings,
    batch_size=batch_size,
    epochs=epochs,
    verbose=1,    
    callbacks=[checkpoint]
)

学習の前に callback の設定をします。ModelCheckpoint はある頻度で、モデルを保存するためのコールバックです。デフォルトで1エポック終了毎に保存されます。コンストラクタの第一引数は保存するファイルパスで、{epoch:03d} は3桁(0埋め)エポック番号になります。verbose は0に設定すると、コールバックが動作してもメッセージを表示しません。

fit メソッドで学習を実行します。第一引数は訓練データ、第二引数は教師データになります。引数 batch_size はバッチサイズ、epochs はエポック数、callbacks はコールバックをそれぞれ指定します。verbose を1に設定すると、学習の進捗状況を表示します。

# 結果表示
plt.plot(history.history["loss"])

predicts = model.predict(ordered_x, 1, verbose=1)
prot_result(ordered_x, ordered_y, predicts)

結果を表示します。history の中に結果が入っているので、それを利用します。history.history["loss"] はエポック毎の損失の推移になります。 predict メソッドは、学習後のニューラルネットワークを用いて出力を予測します。第一引数は入力、第二引数はバッチサイズ、verbose は進捗状況を表示するかどうかです。得られた予測結果 predictsprot_result に渡して、予測結果がどれだけ正解に近いのかを描画します。

結果

結果は下記の通りになりました。まずは誤差の推移です。 縦軸が誤差、横軸がエポックです。前回よりも誤差の下がり方が緩やかです。

続いて、学習後のニューラルネットワークで予測した結果です。 緑の実線が正解のサインカーブ、青の点線がニューラルネットワークで予測した結果です。こちらは20000回学習後のニューラルネットワークで予測した結果ですが、残念ながら芳しくないようです。

では、設定を少し変更して、学習し直してみます。

# 設定
train_num = 10000    # 訓練データ数
batch_size = 10      # バッチサイズ
epochs = 30          # エポック数
neuron_num = 15      # 中間層のニューロン数
model_dir = "/root/practice/model/"

訓練データ数を10000、バッチサイズを10、エポック数を30(1エポックの学習回数は、訓練データ数10000 ÷ バッチサイズ10 = 1000回、総学習回数は、1エポックの学習回数1000 × エポック数30 = 30000回)にします。

# モデル作成
model = Sequential()
model.add(Dense(neuron_num, activation='sigmoid', input_shape=(1,)))
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')

最適化アルゴリズム optimizer を adam に変更します。

この設定で再度学習した結果が下記になります。まずは誤差の推移です。 誤差の減少度合いがやや早くなってますね。

続いて、学習後のニューラルネットワークで予測した結果です。   ニューラルネットワークで予測した結果が、正解のサインカーブとほぼほぼ重なっていることがわかります。

学習途中のニューラルネットワークで予測した結果も表示します。結果を出力するためのソースコードは下記です。

# 途中結果表示用
model_file = os.path.join(model_dir, "001.hdf5")
target_model = load_model(model_file)
target_predicts = target_model.predict(ordered_x, 1, verbose=1)
prot_result(ordered_x, ordered_y, target_predicts)

上から順に、1エポック終了後、10エポック終了後、20エポック終了後のニューラルネットワークが予測した結果になります。   1エポック終了後は、まだまだ学習が足りず正解のサインカーブからかなり離れていますが、学習が進むにつれて正解のサインカーブに近づいているのがわかります。20エポック終了後だと、正解のサインカーブに大分近くなっていることがわかります。

次回は前回学習した分類問題について、TensorFlow・Keras を使って学習していきます。

TensorFlow や Numpy を使わずにニューラルネットワークを実装する 分類編(2)

今回は、前回準備した分類問題用のニューラルネットワークで、実際に、分類問題を学習していきたいと思います。

学習する問題

水平方向にx軸、垂直方向にy軸というxy平面を考えます。x軸方向は-3から3まで、y軸方向は-1から1までとし、このxy平面に等間隔に配置された400個の点(座標)を用意します。このxy平面を y = sin(x) という、いわゆるサインカーブで2つの領域(サインカーブの上か下か)に区切ります。400個の点(座標)がこの2つの領域のどちらにあるのかを学習していきます。文字だけではイメージが湧きにくいかと思いますので、学習前の点(座標)が分類されていない状態の図を用意しました。今回も学習のみになります。

準備(分類用ユーティリティ関数)

学習を始める前に、訓練・教師データの作成や、学習経過を表示するための関数を作成します。

import math
import random

import matplotlib.pyplot as plt

target_func = lambda x: math.sin(x)

def cross_entropy_error(outputs, teachings):
    """クロスエントロピー誤差を計算する

    :param teaching: 教師データ
    :param output: 出力値
    :return: クロスエントロピー誤差
    """
    sum = 0.0
    for output, teaching in zip(outputs, teachings):
        big = max([output, 1e-7])
        sum += teaching * math.log(big)

    return -sum


def create_learning_data():
    """学習用データを作成する

    :return:  訓練データのリストと教師データのリスト
    """
    x = linspace(-3, 3, 20)
    y = linspace(-1, 1, 20)
    random.shuffle(x)
    random.shuffle(y)

    trainings_list = []
    teachings_list = []
    for _x in x:
        value = target_func(_x)

        for _y in y:
            trainings_list.append([_x, _y])
            teaching = [1, 0] if _y >= value else [0, 1]
            teachings_list.append(teaching)

    return trainings_list, teachings_list


def calculate_output_by_current_weights(
    inputs_list, input_layer, middle_layer, output_layer
):
    """現時点の重みで出力を計算する

    :param inputs_list: 入力値のリスト
    :param input_layer: 入力層インスタンス
    :param middle_layer: 中間層インスタンス
    :param output_layer: 出力層インスタンス
    :return: 出力値のリスト
    """
    outputs_list = []
    for inputs in inputs_list:
        input_layer.set_neurons(inputs)
        middle_layer.calculate_output(input_layer.neurons)
        output_layer.calculate_output(middle_layer.neurons)
        outputs_list.append(output_layer.neurons)
    return outputs_list


def create_result_distribution_chart(inputs_list, outputs_list, path):
    """結果の散布図を作成する

    :param inputs_list: 入力値のリスト
    :param outputs_list: 出力値のリスト
    :param path: 散布図を出力するファイルパス
    """
    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)

    x1, y1, x2, y2 = [], [], [], []

    for inputs, outputs in zip(inputs_list, outputs_list):
        _x, _y = inputs
        # どちらに判定されているかで分類する
        if outputs[0] >= outputs[1]:
            x1.append(_x)
            y1.append(_y)
        else:
            x2.append(_x)
            y2.append(_y)

    ax.scatter(x1, y1, s=20, c="blue", marker="*")
    ax.scatter(x2, y2, s=20, c="green")

    # 境界となる線を書く
    pi = 3.141592  # 円周率
    x = linspace(-pi, pi, 100)
    y = [target_func(_x) for _x in x]
    ax.plot(x, y, color="orange")

    plt.savefig(path)

一つ一つ説明していきます。

def cross_entropy_error(outputs, teachings):
    """クロスエントロピー誤差を計算する

    :param teaching: 教師データ
    :param output: 出力値
    :return: クロスエントロピー誤差
    """
    sum = 0.0
    for output, teaching in zip(outputs, teachings):
        big = max([output, 1e-7])
        sum += teaching * math.log(big)

    return -sum

分類問題では、誤差関数としてクロスエントロピー誤差を利用するので、その実装になります。数式通りの実装ですが、オーバーフロー対策がしてあります。

def create_learning_data():
    """学習用データを作成する

    :return:  訓練データのリストと教師データのリスト
    """
    x = linspace(-3, 3, 20)
    y = linspace(-1, 1, 20)
    random.shuffle(x)
    random.shuffle(y)

    trainings_list = []
    teachings_list = []
    for _x in x:
        value = target_func(_x)

        for _y in y:
            trainings_list.append([_x, _y])
            teaching = [1, 0] if _y >= value else [0, 1]
            teachings_list.append(teaching)

    return trainings_list, teachings_list

学習用に訓練データと教師データを作成する関数です。x軸方向に-3から3まで等間隔に20個の点(座標)を生成し、そのx軸の点(座標)それぞれに対してy軸方向に-1から1まで等間隔に20個の点(座標)を生成します。すなわち格子状に等間隔に20 × 20 = 400個の点(座標)を作成するイメージです。順番はランダムにしておきます。生成した点(座標)のy座標と、x座標を target_func() (今回は sin(x))に通した値とを比較(つまり、サインカーブの上にあるか下にあるか)して、教師データを生成します。点(座標)(x, y) がサインカーブより上にあれば、教師データとして[1, 0]を生成し、下にあれば[0, 1]を生成します。これらのデータを trainings_list, teachings_list に詰めて返します。

def create_result_distribution_chart(inputs_list, outputs_list, path):
    """結果の散布図を作成する

    :param inputs_list: 入力値のリスト
    :param outputs_list: 出力値のリスト
    :param path: 散布図を出力するファイルパス
    """
    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)

    x1, y1, x2, y2 = [], [], [], []

    for inputs, outputs in zip(inputs_list, outputs_list):
        _x, _y = inputs
        # どちらに判定されているかで分類する
        if outputs[0] >= outputs[1]:
            x1.append(_x)
            y1.append(_y)
        else:
            x2.append(_x)
            y2.append(_y)

    ax.scatter(x1, y1, s=20, c="blue", marker="*")
    ax.scatter(x2, y2, s=20, c="green")

    # 境界となる線を書く
    pi = 3.141592  # 円周率
    x = linspace(-pi, pi, 100)
    y = [target_func(_x) for _x in x]
    ax.plot(x, y, color="orange")

    plt.savefig(path)

学習の途中経過を散布図で出力するための関数です。matplotlib の説明はここではしません。inputs_list は入力値のリスト(つまり点(座標))、outputs_list は出力値のリストで calculate_output_by_current_weights() で計算・出力された値になります。forループでoutputs_list の中身を取り出して、ニューラルネットワークがどちらに分類したのかを判断していきます。教師データをサインカーブより上にあれば[1, 0]、下にあれば[0, 1]というように生成したので、outputs[0] >= outputs[1] ならば、入力データ点(座標)(x, y)はサインカーブより上、outputs[0] < outputs[1] ならばサインカーブより下、とニューラルネットワークが判定したことになります。入力値のx座標・y座標を2つのグループ、上グループ(x1, y1)と 下グループ(x2, y2) に振り分けていきます。そして、この2グループを散布図として描画し、わかりやすいように境界となるサインカーブも合わせて描画して、ファイルに出力します。

学習

それでは実際に学習していきます。

if __name__ == "__main__":
    input_num = 2    # 入力層のニューロン数
    middle_num = 20  # 中間層のニューロン数
    output_num = 2   # 出力層のニューロン数
    loop_num = 250   # ループ回数 -> 学習回数は loop_num * 学習データ数 になる

    input_layer = InputLayer(input_num)
    middle_layer = MiddleLayer(middle_num, input_num)
    output_layer = OutputLayer(output_num, middle_num)

    trainings_list, teachings_list = create_learning_data()

    indexes, errors = [], []
    index = 0

    for _ in range(loop_num):
        for trainings, teachings in zip(trainings_list, teachings_list):
            # 順伝播
            input_layer.set_neurons(trainings)
            middle_layer.calculate_output(input_layer.neurons)
            output_layer.calculate_output(middle_layer.neurons)

            # 逆伝播
            output_layer.calculate_delta(teachings)
            output_layer.update_weights(middle_layer.neurons)
            output_layer.update_biases()

            middle_layer.calculate_delta(output_layer.deltas, output_layer.weights)
            middle_layer.update_weights(input_layer.neurons)
            middle_layer.update_biases()

            index += 1

            # 途中結果保存
            if index == 1 or index % 10000 == 0:
                indexes.append(index)
                error = cross_entropy_error(output_layer.neurons, teachings)
                errors.append(error)

                # 現時点の重みで計算し結果を散布図にする
                outputs_list = calculate_output_by_current_weights(
                    trainings_list, input_layer, middle_layer, output_layer
                )
                create_result_distribution_chart(
                    trainings_list, outputs_list, f"png/{index}.png"
                )

    # 誤差の推移を出力する
    create_line_graph(indexes, errors, "png/result.png")

前々回の回帰問題とほとんど変わらないソースなので、違う部分のみを記載します。まずは各層のニューロン数ですが、入力は点のx座標とy座標なので2つ、出力は、対象の点(座標)がサインカーブの上か下かどちらに分類したのかを表すものなので2つ、中間層は良い結果が出そうな値(今回は20)を設定します。前々回の回帰問題では生成した学習用データの個数分だけ学習が行われましたが、今回の分類問題では、学習用データが400個しか生成されないため、それをそのまま使うだけでは学習が足りません。そのため、学習データを繰り返し使うためのforループを用意し、その繰り返し回数を loop_num で制御しています。また、誤差関数はクロスエントロピー誤差を利用し、学習の途中経過を散布図で描画していきます。

学習結果

今回の学習回数は、学習データ 400 × ループ回数 250 = 100000回になります。途中経過は10000回毎に出力しています。この設定で学習結果は下記の通りになりました。まずは誤差の推移です。

学習回数10000回くらいまでで、誤差がガクッと落ち、その後も少しずつ誤差が小さくなっていくのがわかります。

続いて学習の経過です。上から順に、学習前・20000万回・40000万回・60000万回・80000万回・100000万回学習した時点のニューラルネットワークによる分類結果です。

学習前は緑色の丸のみで、何も分類されていません。20000万回学習後だとかなり分類されていますが、x座標が-3や3のような端っこは、サインカーブの下に青の星があったり、サインカーブの上に緑の丸があったりします。その後学習が進むにつれて、x座標-3あたりのサインカーブ下の青い星が、緑の丸に変わっていき、x座標3あたりのサインカーブ上の緑の丸が、青い星に変わっていく様子がわかります。このように学習が進むにつれ、少しずつ正解に近づいていく様子がわかります。