IoT技術部の5回目は「音声認識」で命令を聞く「人工知能」に挑戦です。みなさんは、話題の人工知能って手軽にやろうとするとどれくらいできるのだろうって思ったことありませんか?
今回は、Amazon Echoのように、Raspberry Piにつけられたマイクに向かって命令すると、命令を聞くというモノをつくってみようと思います。
今回作った人工知能は、名付けて「トメキチ(留吉)」。名前を考えていてパッと頭に湧いたのが「トメキチ」だった、という単純な理由でこの名前にしましたので深い意味はありません。。。
目標とする動きは、こんな感じです。
1)マイクからの音声を認識する
2)認識した音声に応じてコマンドを実行する今回は、
「トメキチ 歌ってください」
と話しかけると、トメキチが歌い出すようにしてみます。あとは、ちょっと会話っぽくするために、話しかけると
「へい!親方!」
と答えてくれるようにします。原理としては、「トメキチ」が命令開始を意味し、「トメキチ」と呼ばれたことを認識した後、数秒間、コマンドメッセージの音声を受け付ける。という形です。
また、周囲の会話を聞き取って、誤作動しないようにしましょう。最初に「トメキチ」と話しかけずにいきなり「歌ってください」と話しかけても何も認識しないように工夫もしました。
実装の手順は、
Ⅰ.デバイス準備
Ⅱ.OSの設定、ソフトウェアのインストール、設定
- 音声の入出力デバイスの設定
- Juliusのインストール
- Open JTalkインストール
Ⅲ.”トメキチ”プログラムの作成
- Gemfileの作成
- “トメキチ” プログラム
- 歌ってもらう音声ファイルを設定する
- Open JTalk実行用シェルスクリプトの準備
- Julius起動用スクリプトの作成
Ⅳ.”トメキチ”を実行!
- Juliusの起動
- Rubyプログラムの起動
- 話しかけてみよう!
という流れになります。
Ⅰ.デバイス準備
今回使用する構成は、こんな感じです。
▼ハードウェアの構成
・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
Ⅱ. 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が再生されます
さらにLEDを光らせる命令をしてみましょう。
1)ユーザ :「トメキチ」
2)トメキチ:「へい!親方」(LEDが光る)
3)ユーザー:「光ってください」(LEDが光っている間に呼びかける)
4)トメキチ: 「はい、光ってくださいですね」
5)緑のLEDが点滅します。
トメキチのコアコンポーネントは音声認識を司るJuliusですが、これには多数のオプションや設定があり、音声認識の精度や動作を変更できるようです。いろいろな設定を試して自分好みのトメキチを作るのも楽しいのではないでしょうか。
この構成で実現できるのは、すでに設定されている言葉のパターンのみの認識で、かつその言葉に応じた処理を実行するということでした。つまり、決まった言葉にのみ反応するということなので、方言やいろんなイントネーションに対応しようと思うと、まずは音声入力部分のプログラムを作りこむ必要があることがわかりました。
人工知能としても、決まったコマンドだけを実行するというものなのでかなりシンプルです。(人工知能と呼べるのだろうか・・・)でも、少しずつ処理を増やしていくことでどんどんできることも増えて行くことも体験できました。
皆さんなりのトメキチを作って、是非、IoT技術部にも紹介してくださいね!
それにしても、何度も”トメキチ”を会話していると自然と愛着が湧いてくる不思議な感じでした☆
IoTNEWS技術チームです。IoTを取り巻く技術を検証したり、いろいろ作ってみたりします。お仲間も募集しています!