Raspberry Piで音声認識する人工知能をつくってみる

企業:/

IoT技術部の5回目は「音声認識」で命令を聞く「人工知能」に挑戦です。みなさんは、話題の人工知能って手軽にやろうとするとどれくらいできるのだろうって思ったことありませんか?

今回は、Amazon Echoのように、Raspberry Piにつけられたマイクに向かって命令すると、命令を聞くというモノをつくってみようと思います。

今回作った人工知能は、名付けて「トメキチ(留吉)」。名前を考えていてパッと頭に湧いたのが「トメキチ」だった、という単純な理由でこの名前にしましたので深い意味はありません。。。

目標とする動きは、こんな感じです。

1)マイクからの音声を認識する
2)認識した音声に応じてコマンドを実行する

今回は、
「トメキチ 歌ってください」
と話しかけると、トメキチが歌い出すようにしてみます。

あとは、ちょっと会話っぽくするために、話しかけると
「へい!親方!」
と答えてくれるようにします。

原理としては、「トメキチ」が命令開始を意味し、「トメキチ」と呼ばれたことを認識した後、数秒間、コマンドメッセージの音声を受け付ける。という形です。

また、周囲の会話を聞き取って、誤作動しないようにしましょう。最初に「トメキチ」と話しかけずにいきなり「歌ってください」と話しかけても何も認識しないように工夫もしました。

実装の手順は、

Ⅰ.デバイス準備
Ⅱ.OSの設定、ソフトウェアのインストール、設定

  1. 音声の入出力デバイスの設定
  2. Juliusのインストール
  3. Open JTalkインストール

Ⅲ.”トメキチ”プログラムの作成

  1. Gemfileの作成
  2. “トメキチ” プログラム
  3. 歌ってもらう音声ファイルを設定する
  4. Open JTalk実行用シェルスクリプトの準備
  5. Julius起動用スクリプトの作成

Ⅳ.”トメキチ”を実行!

  1. Juliusの起動
  2. Rubyプログラムの起動
  3. 話しかけてみよう!

という流れになります。

Ⅰ.デバイス準備

今回使用する構成は、こんな感じです。

▼ハードウェアの構成

・Raspberry Pi 2
・USBマイク
・スピーカー(Raspberry Pi組み込みのもの)
・ブレッドボード
・LED

▼ソフトウェアの構成

・Ruby 2.1.3
Julius (汎用大語彙連続音声認識エンジン。ユーザーからの音声の認識に使用。)
Open JTalk (日本語テキストから音声を生成する音声合成システム。トメキチからの音声に使用。)
・aplay (wavファイルの再生に使用。)

Julius、Open JTalkの仕組み等の詳細については、それぞれのリンクでご確認ください。

なお、本記事で利用するRuby 2.1.3はrbenv を利用して事前にインストールしてあるものとします。

また、aplayはRaspbianに同梱のものを利用しました。

 

Raspberry Piのデバイス作成

用意したもの

  • Raspberry Pi 2(インストール済み)
  • BUFFALO WIFI(設定済み)
  • ブレッドボード
  • LEDライト(赤)
  • LEDライト(緑)
  • ジャンパーワイヤー(オス-メス) × 3

電子工作の手順

  • ジャンパーワイヤー赤はRaspberry:GPIO21/ブレッドボード:h列の23
  • ジャンパーワイヤー黄はRaspberry:GPIO25/ブレッドボード:i列の2
  • ジャンパーワイヤー青はRaspberry:Ground/ブレッドボード:- (マイナス)
  • LED(赤)はカソード(足が短い)をブレッドボード:の– (マイナス)とアノード(足が長い)をj列の2
  • LED(緑)はカソード(足が短い)をブレッドボード:の– (マイナス)とアノード(足が長い)をj列の23

Rsspberry Pi
 

Ⅱ. OSの設定、ソフトウェアのインストール、設定

1.音声の入出力デバイスの設定

・/etc/modules に以下を記載します。

snd-bcm2835 # 内蔵サウンドチップ(ヘッドホンジャック)
snd-pcm-oss # /dev/dsp, /dev/dsp1 を生成(Juliusで使用)

・/etc/modprobe.d/alsa-base.conf に以下に変更します。
※indexが重要です。

options snd-usb-audio index=1 #USBマイク(/dev/dsp1に割り当てられます)
options snd_bcm2835 index=0 #優先

2.juliusのインストール

・http://julius.sourceforge.jp/ からJulius本体のソースコード、ディクテーションキット、文法認識キットをダウンロードします。
今回は、以下のものを使用しました。
・julius-4.2.3.tar.gz
・dictation-kit-v4.2.3.tar.gz
・grammar-kit-v4.1.tar.gz

juliusのコンパイル

# cd ~
# tar xvzf <ダウンロードしたディレクトリ>/julius-4.2.3.tar.gz
# cd julius-4.2.3
# ./configure
# make
# cd julius-simple
# make

・ディクテーションキット、文法認識キットを展開します。今回は~/julius-kits というディレクトリを作成し、その中に展開することとします。

# mkdir -p ~/julius-kits
# cd ~/julius-kits
# tar xvzf <ダウンロードしたディレクトリ>/dictation-kit-v4.2.3.tar.gz
# tar xvzf <ダウンロードしたディレクトリ>/grammar-kit-v4.1.tar.gz

julius 設定

認識する単語の登録をおこないます。(単語辞書ファイル)
juliusに認識させたい単語のリストを作成し、juliusで認識可能な辞書形式にします。

まず、”表示用の単語 ひらがなよみ”のペアを単語毎に1行ずつ記述します。
今回は、ファイル名を ~/tomekichi.yomi として作成しました。(文字コードはUTF-8)

以下が設定内容になります。

留吉 とめきち
歌ってください うたってください
寝てください ねてください
光ってください ひかってください
おわり おわり
消えろ きえろ

次に、これを辞書形式(.dic)に変換します。
Juliusが認識する文字コードはEUC-JPであるため、以下では、
iconvコマンドでUTF-8からEUC-JPに変換した後yomi2voca.plで辞書形式に変換しています。

# cd julius-4.2.3/gramtools/yomi2voca/
# iconv -f utf8 -t eucjp ~/tomekichi.yomi | ./yomi2voca.pl > julius-kits/dictation-kit-v4.2.3/tomekichi.dic

julius用設定ファイルの作成

上記で作成した単語辞書ファイルなどを指定する設定ファイルを作成します。

/root/julius-kits/dictation-kit-v4.2.3/tomekichi.jconf

-w tomekichi.dic
-v model/lang_m/web.60k.htkdic
-h model/phone_m/hmmdefs_ptm_gid.binhmm
-hlist model/phone_m/logicalTri
-n 5
-output 1
-input mic
-zmeanframe
-rejectshort 800
-charconv EUC-JP UTF-8

3.Open JTalkインストール

Open JTalkは、”留吉”に話をさせるために導入します。
※引数で渡された文字をwavに変換してくれます。

Open JTalk関連のパッケージをインストール

apt-get install open-jtalk open-jtalk-mecab-naist-jdic htsengine libhtsengine-dev hts-voice-nitech-jp-atr503-m001

Ⅲ. “トメキチ”プログラムの作成

さて、環境が整ったところで、”トメキチ”を作成していきます。今回使用するプログラミング言語はRubyです。

1. Gemfileの作成

トメキチで使用するRuby用のライブラリをインストールするために、Gemfileを作成します。
Gemfileの中身は以下のとおりです。

~/src/tomekichi/src/Gemfile

source 'https://rubygems.org'
gem 'julius'

Gemfile作成後、以下のコマンドを実行し、Ruby用のライブラリをインストールします。

# bundle install

実行後、Gemfile.lockというファイルができます。このファイルは消さないでください。

2.”トメキチ” プログラム

以下の3つのファイルを作成します。

* ~/src/tomekichi/src/tomekichi.rb (本体)

SRC_DIR = File.expand_path(File.dirname(__FILE__))

require 'rubygems'
require 'julius'
require File.join(SRC_DIR, '/l_chika')
require File.join(SRC_DIR, '/command')

class Tomekichi
  def run(julius, l_chika)
    waiting = Time.now
    commands = Commands.defaults

    @child_pid = nil

    begin
      julius.each_message do |message, prompt|
        case message.name
        when :RECOGOUT
          prompt.pause
        
          shypo = message.first
          whypo = shypo.first
          confidence = whypo.cm.to_f
  
          puts "#{message.sentence} #{confidence}"
          # confidenceの値が大きいほど、認識の確度が高い。認識率を変えたい場合、この条件を返るのが一番簡単な方法
          if confidence > 0.90 
            result = commands.evaluate(message.sentence)
            case result[:status]
            when :finished then
              if result[:pid]
                kill_child # kill previous child process
                @child_pid = result[:pid]
              end
              l_chika.off
              waiting = Time.now
              commands = Commands.defaults
            when :forward then
              l_chika.on
              waiting = Time.now
              commands = result[:sub_commands]
            when :kill_child
              kill_child
              l_chika.off
              waiting = Time.now
              commands = Commands.defaults
            when :unknown
              current = l_chika.on?
              l_chika.toggle(!current)
              sleep 0.3
              l_chika.toggle(current)
            end
          end
          prompt.resume
        end
    
        if (Time.now - waiting) > 5
          commands = Commands.defaults
          waiting = Time.now
          l_chika.off
        end
      end
    rescue REXML::ParseException
      puts "retry..."
      retry
    end
  end

  def kill_child
    if @child_pid && process_exists?(@child_pid)
      puts "Killing #{@child_pid}..."
      Process.kill('QUIT', @child_pid)
    end
    @child_pid = nil
  end

  def process_exists?(pid)
    gid = Process.getpgid(pid)
    return true
  rescue => ex
    puts ex
    return false
  end
end

LChika.open do |l_chika|
  puts "接続中..."

  julius = Julius.new

  puts "呼びかけて!"

  tomekichi = Tomekichi.new
  tomekichi.run(julius, l_chika)
end

* ~/src/tomekichi/src/l_chika.rb (LED点灯用ライブラリ)

class LChika
  attr :port, :is_on

  def self.open(port = 25)
    LChika.new(port) { |l_chika|
      yield(l_chika)
    }
  end

  def on
    toggle(true)
  end

  def on?
    @is_on
  end

  def off
    toggle(false)
  end

  def toggle(is_on)
    File.open("/sys/class/gpio/gpio#{@port}/value", "w") do |v|
      v.write is_on ? 1 : 0
    end
    @is_on = is_on
  end

  protected

  def initialize(port=25)
    @port = port
    File.open("/sys/class/gpio/export", "w") do |io|
      io.write(@port)
    end
    dir = File.open("/sys/class/gpio/gpio#{@port}/direction", "w") do |dir|
      dir.write("out")
    end

    yield(self)
  ensure
    self.class.cleanup(@port)
  end

  class << self
    def cleanup(port)
      File.open("/sys/class/gpio/unexport", "w") do |unexport|
        unexport.write(port)
      end
    end
  end
end

* ~/src/tomekichi/src/command.rb (コマンド処理用ライブラリ)

SRC_DIR = File.expand_path(File.dirname(__FILE__))

class Command
  attr :name, :sub_commands, :voice, :proc, :kill_child

  def initialize(args = {})
    @name = args[:name]
    @sub_commands = args[:sub_commands]
    @voice = args[:voice]
    @proc = args[:proc]
    @kill_child = args[:kill_child]
  end

  def execute
    if voice
      talk(voice)
    end

    if proc
      pid = @proc.call
      result = { status: :finished }
      result = result.merge(pid: pid) if pid 
      return result
    elsif @sub_commands
      return { status: :forward, sub_commands: @sub_commands }
    elsif @kill_child
      return { status: :kill_child }
    else
      return { status: :finished }
    end
  end

  def match?(str)
    @name == str
  end

  private

  def talk(str)
    system(File.expand_path(File.dirname(__FILE__)) + '/extensions/jtalk.sh', str)
  end
end

class Commands
  include Enumerable
  extend Forwardable

  def initialize(elements = [])
    @commands = elements
  end

  def add(e)
    @commands << e
    self
  end
  alias_method :<<, :add

  def each
    @commands.each { |e| yield(e) }
  end

  def evaluate(name)
    raise ArgumentError unless name
    command = @commands.find{ |c| c.match?(name) }
    if command
      command.execute
    else
      { status: :unknown }
    end
  end

  def self.defaults
    commands = {}
    commands[:sing] = Command.new(name: '歌ってください', voice: "はい、歌ってくださいですね。", proc: -> {
       pid = spawn("aplay", "-q", File.expand_path(File.dirname(__FILE__)) + '/samples/test.wav')
       pid
    })

    commands[:light] = Command.new(name: '光ってください', voice: "はい、光ってくださいですね。", proc: -> {
       LChika.open(21) { |lc|
         10.times.each do |i|
           i % 2 == 0 ? lc.on : lc.off
           sleep 0.2
         end
         lc.off
       }
    })
    
    commands[:sleep] = Command.new(name: '寝てください', voice: "もう一度、寝てくださいと言ってみてください。", sub_commands: Commands.new([
      Command.new(name: '寝てください', voice: "はい、わかりました。寝れば良いんでしょ?", proc: -> {
        exit
      })
    ]))
    
    commands[:end] = Command.new(name: 'おわり', voice: "はい、おわりですね。", kill_child: true)    
    commands[:shutoff] = Command.new(name: '消えろ', voice: "はい、消えろですね。何もしません。")    
    commands[:init] = Command.new(name: '留吉', voice: 'へい、親方', sub_commands: Commands.new(commands.values))
    cmds = Commands.new
    cmds << commands[:init]
  end
end

3.歌ってもらう音声ファイルを設定する

「トメキチ 歌ってください」と呼びかけた時に、トメキチ に歌ってもらうファイルが必要になるので
お気に入りのwavファイルを以下のように設置します。

/root/src/tomekichi/src/sample/test.wav

4.Open JTalk実行用シェルスクリプトの準備

プログラム内から留吉に話をさせるために Open JTalkを使用します。
Open JTalkには”open_jtalk”コマンドが用意されていますが、これには多数のオプションがあり、実行する都度、オプションを指定するのは大変なため、デフォルトのオプションを予め指定したシェルスクリプトを作成します。

・ ~/src/tomekichi/src/extensions/jtalk.sh

#!/bin/bash
## male
HV=/usr/share/hts-voice/nitech-jp-atr503-m001
## female
# HV=/root/src/jtalk_mei/MMDAgent_Example-1.3.1/Voice/mei_happy

tempfile="/tmp/$1.wav"
echo $tempfile
option="-td $HV/tree-dur.inf \
  -tf $HV/tree-lf0.inf \
  -tm $HV/tree-mgc.inf \
  -md $HV/dur.pdf \
  -mf $HV/lf0.pdf \
  -mm $HV/mgc.pdf \
  -dm $HV/mgc.win1 \
  -dm $HV/mgc.win2 \
  -dm $HV/mgc.win3 \
  -df $HV/lf0.win1 \
  -df $HV/lf0.win2 \
  -df $HV/lf0.win3 \
  -dl $HV/lpf.win1 \
  -ef $HV/tree-gv-lf0.inf \
  -em $HV/tree-gv-mgc.inf \
  -cf $HV/gv-lf0.pdf \
  -cm $HV/gv-mgc.pdf \
  -k $HV/gv-switch.inf \
  -s 16000 \
  -p 75 \
  -a 0.03 \
  -u 0.0 \
  -jm 1.0 \
  -jf 1.0 \
  -jl 1.0 \
  -x /var/lib/mecab/dic/open-jtalk/naist-jdic \
  -ow $tempfile"

if [ -z "$1" ] ; then
  open_jtalk $option
else 
  if [ -f "$1" ] ; then
    open_jtalk $option $1
  elif [ ! -f "$tempfile" ] ; then
    echo "$1" | open_jtalk $option
  fi
fi

aplay -q "$tempfile"
rm $tempfile

・作成したファイルに実行許可を与えます。

# chmod +x ~/src/tomekichi/src/extensions/jtalk.sh

 

5.Julius起動用スクリプトの作成

Julius起動時のオプション等を毎回指定するのが面倒なので、以下の様なスクリプトを作成します。

  • ~/julius_server.sh (julius起動用スクリプト)
#!/bin/bash
AUDIODEV=/dev/dsp1 ~/julius-4.2.3/julius/julius -C ~/julius-kits/dictation-kit-v4.2.3/tomekichi.jconf -module

作成後、実行許可を与えます。

# chmod +x ~/julius_server.sh

 

Ⅳ.”トメキチ”を実行!

準備完了です! それでは早速トメキチを動かしてみましょう!

1.Juliusの起動

Raspberry Pi にssh経由でログイン後、julius_server.shを実行します。

# ~/julius_server.sh

2.Rubyプログラムの起動

・もう一つ、別のssh接続を行い、先ほど作成したrubyプログラムを実行します。(bundleコマンドは、Rubyをインストール後、’gem install bundler’を実行するとインストールできます。)

# cd ~/src/tomekichi/src
# bundle exec ruby tomekichi.rb

3.話しかけてみよう!

では、マイクに向かって話しかけてみましょう。
すべてうまく行っていれば、次のように動作します。

「トメキチ」を話しかけると
“トメキチ”が「へい!親方」と答えます。
と、同時にLEDが光り、5秒放置するとLEDが消えます。

では、歌わせてみましょう。
1)ユーザ :「トメキチ」
2)トメキチ:「へい!親方」(LEDが光る)
3)ユーザー:「歌ってください」(LEDが光っている間に呼びかける)
4)トメキチ: 「はい、歌ってくださいですね」
5)samples/test.wavが再生されます

Raspberry Pi + Juliusで音声認識する人工知能をつくる

さらにLEDを光らせる命令をしてみましょう。
1)ユーザ :「トメキチ」
2)トメキチ:「へい!親方」(LEDが光る)
3)ユーザー:「光ってください」(LEDが光っている間に呼びかける)
4)トメキチ: 「はい、光ってくださいですね」
5)緑のLEDが点滅します。

Raspberry Pi + Juliusで音声認識する人工知能を作る

トメキチのコアコンポーネントは音声認識を司るJuliusですが、これには多数のオプションや設定があり、音声認識の精度や動作を変更できるようです。いろいろな設定を試して自分好みのトメキチを作るのも楽しいのではないでしょうか。

この構成で実現できるのは、すでに設定されている言葉のパターンのみの認識で、かつその言葉に応じた処理を実行するということでした。つまり、決まった言葉にのみ反応するということなので、方言やいろんなイントネーションに対応しようと思うと、まずは音声入力部分のプログラムを作りこむ必要があることがわかりました。

人工知能としても、決まったコマンドだけを実行するというものなのでかなりシンプルです。(人工知能と呼べるのだろうか・・・)でも、少しずつ処理を増やしていくことでどんどんできることも増えて行くことも体験できました。

皆さんなりのトメキチを作って、是非、IoT技術部にも紹介してくださいね!

それにしても、何度も”トメキチ”を会話していると自然と愛着が湧いてくる不思議な感じでした☆

Previous

UBICとRetty、サムライインキュベート、「食」をテーマに人工知能ハッカソンを開催

ゼネリックソリューション、千葉銀と協業で、人工知能技術を使った金融ビッグデータ分析業務開始

Next