nikkie-ftnextの日記

イベントレポートや読書メモを発信

翔泳社祭2022をPythonとjqを使って満喫する

はじめに

千葉の名物、踊りと祭り!同じ阿呆なら踊らにゃ sing a song! nikkieです。

先日(2/17,18)デブサミがあったんですね。
ということは、翔泳社翔泳社さんの電子書籍が50%割引で買えるセール)も開催中だ!(2/24(木)までだそうです)

昨年は「Pythonを使って満喫」しましたが、今年はアップデート版をお送りします。

目次

Pythonとjqを使って満喫する流れ

  1. 公開されているスプレッドシート1のデータを取得し、ローカルに保存
  2. 条件を指定して、検索(今回はjqを使います)

f:id:nikkie-ftnext:20220220155056p:plain

お祭り参加準備

read onlyで公開されているスプレッドシートからデータを取得するために、OAuthクライアントIDを準備します(手順は2021の記事を参照ください)。
オーバーエンジニアリングと感じられた方は、csvやxlsx形式でダウンロードしてもいいと思います2
スプレッドシートが公開されているのはとてもありがたいですね。

動作環境

Pythonの動作環境として以下を準備しました。

gspread              5.1.1
jsonlines            3.0.0

2021から両ライブラリともメジャーバージョンアップしていますね!
gspreadはv3 → v5、jsonlinesはv2 → v3)

jqのバージョンは以下です。

$ jq --version
jq-1.6

公開されているスプレッドシートのデータ取得

2021同様、gspreadで取得します。

import gspread
import jsonlines

client = gspread.oauth()
spreadsheet = client.open_by_key(
    "1vS3-vAVH7-vRMaDP_c_rRf2uKHq-yjQGTKr0Cpggz2U"
)
worksheet = spreadsheet.worksheet("対象一覧")  # 2021とワークシート名が異なります

list_of_dicts = worksheet.get_all_records(head=2)  # ヘッダーは2行目と指定
price_sorted = sorted(
    list_of_dicts, key=lambda row: row["定価\n (本体価格+税)"], reverse=True
)

with jsonlines.open("shoeisha_fes_2022.jsonl", "w") as writer:
    writer.write_all(price_sorted)
  • jqコマンドを練習したく3JSON Lines形式で書き出しました
  • ヘッダが2行目であることを指定します(2行目の各セルの値が辞書のキーになります)
  • 価格順に並べ替えたいのですが、jqでできるかぱっと分からず、取得したデータを価格の降順に並べ替えてから保存することにしました

対象本データを見て

対話モードで確認してみました(pythonコマンドの-iオプション4)。

カテゴリ①(M列)の取る値

>>> from collections import Counter
>>> category1_counter = Counter(book["カテゴリ①"] for book in list_of_dicts)
>>> for category1, count in category1_counter.most_common():
...   print(f"{category1}: {count}")
...
PC技術_専門: 372
ビジネス_専門: 234
IT資格: 167
PC技術_入門: 161
クリエイティブ: 137
ビジネス_入門: 125
PC一般: 119
生活実用: 93
一般資格: 88
理工書: 71
医療福祉: 62
福祉資格: 42
一般書: 36

PC技術本にしぼった時にカテゴリ②(N列)の取る値

PC技術_専門(372冊)とPC技術_入門(161冊)、合計533冊の内訳です。

>>> tech_category2_counter = Counter(book["カテゴリ②"] for book in list_of_dicts if book["カテゴリ①"].startswith("PC技術"))
>>> for category2, count in tech_category2_counter.most_common():
...   print(f"{category2}: {count}")
...
プログラミング・開発: 211
ネットワーク・サーバ: 89
開発管理: 69
Webプログラミング: 52
人工知能・機械学習: 36
データベース: 35
LINUX・UNIX: 15
データサイエンス: 11
ハードウェア・自作: 9
パソコンソフト: 3
パソコン一般: 3

jqでフィルタ

shoeisha_fes_2022.jsonl がスプレッドシートJSON Lines形式で書き出したファイルです。

$ jq -c 'select(."カテゴリ①" | startswith("PC技術"))' shoeisha_fes_2022.jsonl > technical_books_2022.jsonl
$ jq -c 'select(."Kindle 50%割引" | contains("https://www.amazon.co.jp/"))' technical_books_2022.jsonl > kindle_technical_2022.jsonl
  • カテゴリ①が「PC技術_専門」「PC技術_入門」の書籍を抜き出し、technical_books_2022.jsonlに保存
  • Kindleで買える(=AmazonのURLが記載されている)書籍を抜き出し、kindle_technical_2022.jsonlに保存
    • Kindleで買えない書籍は「対象外」記載のようです

例えば、書名に「Python」と入っている書籍を抽出してみましょう。
(表記ゆれを警戒し(※気にし過ぎかも)、小文字に揃えてから、「python」を含むかで抜き出します)

$ jq -c 'select(."商品名" | ascii_downcase | contains("python"))' kindle_technical_2022.jsonl > python_kindle_technical_2022.jsonl

wc -lで表示した行数

    1707 shoeisha_fes_2022.jsonl
     533 technical_books_2022.jsonl
     475 kindle_technical_2022.jsonl
      35 python_kindle_technical_2022.jsonl

Pythonに関係する本

JSON Linesから見たいフィールドだけを抜き出します。
1行に、値段、Amazonへのリンク、書名の3点を表示しています。
値段に単位をつけたり、書名を『』でくくったりするためにjoinを使いました。

$ jq -r '[([."定価\n (本体価格+税)", "円"] | join("")), ."Kindle 50%割引", (["『", ."商品名", "』"] | join(""))] | join(" ")' python_kindle_technical_2022.jsonl

出力を見ると、みんなのPython勉強会(stapy)で取り上げた本がいくつも見つかりました!
「おー、あの回で紹介された本だー!」と一スタッフとして嬉しくなり、続く紹介コーナーを書きました。

『実践Django

2021/08のstapyで著者の芝田さんに話していただきました。

めちゃくちゃ詳しくて学びがたくさんなこの本、中でもDjangoのテストの書き方が私には役立ってます。

『スラスラわかるPython 第2版』

先月(2022/01)のstapyで著者の岩崎さんに話していただきました5

私は対象読者からは外れていると思うのですが、「Pythonを学んでみたい」方はこの機にいかがでしょうか?
半額ですし、2版が出るということはそれなりに売れていて定評があるということだと思います。

Python FlaskによるWebアプリ開発入門』

2022/01のstapy懇親会で知った本!

気になっていたので、半額のこの機に購入ですね。

『AIエンジニアのための機械学習システムデザインパターン

著者の澁井さんは、2021/06にstapy懇親会でLT、2021/07のstapy外伝でティアフォーさんのMLOpsを紹介されていました6

機械学習モデルをサーブするAPIk8sクラスタにデプロイするということを日常的にやっている身としては、この本からIstioがめちゃくちゃ便利だと思い知らされました。

『テスト駆動Python

最近読んでいて、pytestのキャッチアップにオススメの一冊。

監修のやっとむさん(安井 力さん)には、2019/06のstapyで話していただきました。
https://youtu.be/T7Tqt0fJG1A?t=4600 からアーカイブを見られます。

『見て試してわかる機械学習アルゴリズムの仕組み 機械学習図鑑』

2019/08のstapyで紹介された1冊。

https://youtu.be/gKrQVJ1v7WM?t=2654 からアーカイブを見られます。

この本は、2019年に「Google のエンジニアたちが選んだプログラミングに関する書籍 10 冊」(中学校及び高等学校に寄贈)にも入っています!

本当に分かりやすくて、実は私もこっそり読んでいます。

終わりに

翔泳社さん、今年もスプレッドシートの公開、ありがとうございます。
全然本屋さんに行かなくなってしまいましたが、書店の大きな本棚の前に立っている感覚でした(セール品以外は目に入らない状況でもありますが)。
昨年も書きましたが、Webへの公開を検討いただけると嬉しいです(JSONで取得できると、翔泳社祭、もっと盛り上がると思うんです!)

jqコマンドの素振りにもなりましたね。
jqコマンド、わざわざPython書かなくてもJSON Linesをフィルタできるので結構いい感じです。
これはまたの機会にアウトプットしたいですね。

ザッピングを終えたので、あとは2/24(木)までに吟味して購入しようと思います。
気になる書籍が多すぎて、ヨサンハンイナイデナンテエラベナイヨー

参考までに、ザッピングコマンド

$ jq -r '[([."定価\n (本体価格+税)", "円"] | join("")), ."Kindle 50%割引", (["『", ."商品名", "』"] | join("")), ."発売日"] | join(" ")' technical_books_2022.jsonl > technical_books_2022.txt

  1. https://docs.google.com/spreadsheets/d/1vS3-vAVH7-vRMaDP_c_rRf2uKHq-yjQGTKr0Cpggz2U/edit#gid=0

  2. Pythoncsv.DictReaderを使って読み込んで、JSON Lines形式で書き出し、この記事に合流できると思います。

  3. JSON Linesをjqコマンドで扱う方法、『jqハンドブック』で完全に理解した! - nikkie-ftnextの日記

  4. スクリプトかコマンドを実行した後にインタラクティブモードに入ります。」 https://docs.python.org/ja/3/using/cmdline.html#cmdoption-i

  5. トークは大変興味深く、そこで出た質問を調べてみたら学びがありました。👉 1月の #stapy で挙がった2つの質問に回答します:キーワード専用引数はいつから? 位置専用引数の使い所は? - nikkie-ftnextの日記

  6. 直近の 第16回 MLOps 勉強会(Online) - connpass によると、澁井さん現在はLaunchable, Inc.在籍のようです

声をPythonに聴かせて(後編:対処し、マイクの音声でも「変じゃないよ」)

はじめに

私の熱い『アイの歌声を聴かせて』ファン活動、アイカツ、はじまります!

この記事は、アイの歌声を聴かせてに関する一技術者なりのファン活動の一幕です。

"ポンコツAI"シオンの「音声を認識してテキストに変換する」機能の実装アウトプットの後編です。
「シオンが変だ!」(=マイクから入力した音声の書き起こしがイマイチ)を解決します。

espnet_model_zoospeech_recognition.Microphone、動作環境については前編を参照ください。

目次

TL; DR(ただし、Workaround)

import tempfile

import numpy as np
import soundfile as sf
import speech_recognition as sr
from espnet2.bin.asr_inference import Speech2Text
from scipy.io import wavfile

speech2text = Speech2Text.from_pretrained(
    "kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave"
)

r = sr.Recognizer()
with sr.Microphone(sample_rate=16_000) as source:
    print("なにか話してください")
    audio_data = r.listen(source)

frame_bytes = audio_data.get_raw_data()
speech_array = np.frombuffer(frame_bytes, dtype=np.int16)  # (1)
with tempfile.NamedTemporaryFile() as tempf:
    wavfile.write(  # (2)
        tempf.name,
        audio_data.sample_rate,
        speech_array,
    )
    audio_array, sampling_rate = sf.read(tempf.name)  # (3)

nbests = speech2text(audio_array)
text, tokens, *_ = nbests[0]
print(text)
  • マイクから入力した音声データ(bytes)をnp.int16のarrayに変換:(1)
  • scipy.io.wavfileを使い、np.int16のarrayを一時ファイルとして保存:(2)
  • soundfile.readで、一時ファイルをnp.floatのarrayとして読み込み:(3)

今回の学習済みモデルにはnp.floatのarrayを入力する必要がある

うまくいくケース

  • wavファイルをsoundfile.readで読み込み
  • arrayのdtypeはnp.float64

うまくいかないケース

  • マイクで受け取った音声を表すbytesをnp.frombufferでarrayに変換
  • arrayのdtypeはnp.int16

再現実験

2つの「こんにちは」のファイルを用意(man sayを参照しました)

say こんにちは -o hello.wav --data-format=LEF32@16000
say こんにちは -o i16_hello.wav --data-format=LEI16@16000

scipy.io.wavfileで読み込むとdtypeが異なる

>>> from scipy.io import wavfile
>>> sample_rate, frame_data = wavfile.read("i16_hello.wav")
>>> sample_rate2, frame_data2 = wavfile.read("hello.wav")
>>> frame_data.dtype, frame_data2.dtype
(dtype('int16'), dtype('float32'))

np.int16のarrayはうまく書き起こせない

>>> from espnet2.bin.asr_inference import Speech2Text
>>> speech2text = Speech2Text.from_pretrained("kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave")
>>> text, tokens, *_ = speech2text(frame_data2)[0]  # np.float32の方
>>> text
'今日は'

>>> text2, tokens2, *_ = speech2text(frame_data)[0]  # np.int16の方
>>> text2
'で'

soundfileで読み込むとdtypeがfloatに揃う(ので、どちらの音声も書き起こせる)

>>> import soundfile
>>> speech_array, sampling_rate = soundfile.read("i16_hello.wav")
>>> speech_array2, sampling_rate2 = soundfile.read("hello.wav")
>>> speech_array.dtype, speech_array2.dtype
(dtype('float64'), dtype('float64'))

解決するために、bytesをnp.floatのarrayに変換したい

ここがよく分からず、tempfileを使う方法が動作したのでそれで実装しました。
音声まわりの伸びしろがあるので、『Pythonで学ぶ音声認識』などで知識を補ってから、簡潔に書けるか考えてみます。

  • Microphonelistenして得られたAudioData
  • 音声を表す部分はAudioData.get_raw_data()で取得されるbytes
  • np.frombufferはdtypeのデフォルトがfloat
    • floatのarrayに変換してもテキストの書き起こしはうまくいかない
    • floatのarrayはsounddeviceplay1しても音が聞こえない(個々の要素の値がうまく変換できていないということ?)
  • soundfileのドキュメントにio.BytesIOを使う方法2もあったが、エラーが送出され未解決

動いた方法(=先のWorkaround)

  • AudioData.get_raw_data()で得られるbytesはnp.int16のarrayに変える(再生して音が聞こえる変換)
  • np.int16のarrayはscipy.io.wavfileで保存
  • soundfileで読み込み、モデルが要請するnp.floatを満たす

終わりに

シオン v0.0.1の音声認識機能は、(動かすのを優先した実装ではありますが、)スタンドアローンでも動作するようになりました!

公開されている機械学習モデル、すごくないですか!?
ブラックボックスなんですが、読み上げ・音声認識と実装できましたよ!

音声の扱いは広大な伸びしろが広がっているので、習熟してシオンを改良していきたいですね。

v0.0.1として残るのは、会話のエンジンですね(現在はオウム返し)。
これは「我に策あり」です!

ちょっとしたところだと、スクリプト実行したあと入力を受け付けるようになったら、シオンっぽく知らせたいなあ(「おはよう!」とか)

変更履歴

  • 2022/07/11: 「TL; DR(ただし、Workaround)」にてバグ修正(nbests = speech2text(speech_array)nbests = speech2text(audio_array)
    • dtypeをfloat64に変換する前のarrayを渡すコードになっていた

声をPythonに聴かせて(前編:wavファイルだと書き起こせるのに、マイクの入力はいまいち!?)

はじめに

私の熱い『アイの歌声を聴かせて』ファン活動、アイカツ、はじまります!

この記事は、アイの歌声を聴かせてに関する一技術者なりのファン活動の一幕です。

実は私、大好きなアイうたに出てくる"ポンコツAI"シオンを実装しようとしています。
今回は音声を認識してテキストに変換する方法について、これまでに分かったことを書き留めます。
クラウドベンダーが提供するAPIを使っていましたが、音声合成と同様に、機械学習モデルを使った実装に切り替えました(入門レベルであり、「やってみた系」です)。
アイうた劇中と重なる構成でアウトプットできることに気付いたため、2回に分けてお送りします。

目次

これまでのシオン・プロジェクト

  • シオン v0.0.1を3月末までに作るのを目標に進めています
  • 現在のシオン v0.0.1の実装
    1. 人が音声で入力
    2. 音声を文字に起こす
    3. 文字列を処理(今はオウム返し)
    4. 処理された文字列を読み上げる
  • 4の部分について、macOSsay コマンドを、ttslearn機械学習モデルを使った音声合成に更新しました

今回は2の部分をアップデートします。

音声を文字に起こす

これまでの実装:Cloud Speech-To-Text API

GoogleのCloud Speech-To-Text APIを使っていました。
https://cloud.google.com/speech-to-text

ライブラリ SpeechRecognition を使い、「マイクで受け取った音声をCloud Speech-To-Text APIに送り、返り値を受け取る」という実装です。

import speech_recognition as sr

r = sr.Recognizer()
with sr.Microphone() as source:
    print("なにか話してください")
    audio = r.listen(source)

recognized_text = r.recognize_google_cloud(
    audio, credentials_json=credentials_json, language="ja-JP"
)
print(recognized_text.strip())

SpeechRecognitionMicrophone はよくできていて、これだけのコードで プログラムに音声で入力 できます。
音声データはCloud Speech-To-Text APIに送られ、書き起こしたテキストが手に入ります。
なお、Cloud Speech-To-Text APIを使うための初期設定やcredentials_jsonの扱いは、ここでは省略しています。

これで最初に作る分には大成功1なのですが、シオンはスタンドアローン(通信できない)なので、APIを使わない方法を探す必要がありました。
そこで今回、機械学習モデルを使う方法に切り替えたわけです。

機械学習モデルを使った現在の実装

以下の学習済みモデルを利用しています。
https://huggingface.co/espnet/kan-bayashi_csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave

これをロードし、マイクで受け取った音声データを渡して、書き起こしたテキストを得ます。

順を追って、構成要素を紹介します。

espnet_model_zoo

ESPnetについては、kan-bayashiさん(ESPnetの開発者であり、上記のモデルの作成者)のチュートリアルが分かりやすいです。

また、同様の学習済みモデルをロードして使う例は、レトリバさんのブログにまとまっていました。

from espnet2.bin.asr_inference import Speech2Text

speech2text = Speech2Text.from_pretrained(
    "kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave"
)

動作確認:sayコマンドでwavファイルを作る

レトリバさんのブログにある「親譲りの無鉄砲で子供の時から損ばかりしている」(『坊っちゃん』ですね)の音声ファイルは、macOSのsayコマンドで作りました2

$ say 親譲りの無鉄砲で子供の時から損ばかりしている -o sample.wav --data-format=LEF32@16000

@以降はサンプリングレートです(man say参照)。
今回のモデルは16000のデータで訓練されています3

SoundFileというライブラリを使って、wavファイルをNumPy arrayとして読み込みます。
学習済みモデルに渡すと、テキストが書き起こされます。

import soundfile

speech_array, sampling_rate = soundfile.read("sample.wav")

nbests = speech2text(speech_array)
text, tokens, *_ = nbests[0]
print(text)

Microphoneから取得した音声データを書き起こす

wavファイルの音声データを書き起こす方法は分かりました。
speech_recognition.Microphoneから取得した音声データもNumPy arrayにして学習済みモデルに渡せばよさそうですね。

Microphonelistenした返り値は、AudioData型です4
AudioDataインスタンスget_raw_dataメソッドで、音声データをbytes型で得られます5

これらをつなぎ合わせたのが以下のコード:

import numpy as np
import speech_recognition as sr
from espnet2.bin.asr_inference import Speech2Text

speech2text = Speech2Text.from_pretrained(
    "kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave"
)

r = sr.Recognizer()
with sr.Microphone(sample_rate=16_000) as source:
    print("なにか話してください")
    audio_data = r.listen(source)

frame_bytes = audio_data.get_raw_data()
speech_array = np.frombuffer(frame_bytes, dtype=np.int16)

nbests = speech2text(speech_array)
text, tokens, *_ = nbests[0]
print(text)

np.frombufferはbytesをarrayに変えます。
https://numpy.org/doc/stable/reference/generated/numpy.frombuffer.html

ところが、「シオンが変だ!」

ワクワクしながらスクリプトを実行し、マイクから「こんにちは」と入力します。
坊っちゃん』の一節が書き起こせたのですから、「こんにちは」の書き起こしは余裕なはず!
ところが、現れたのは

'で'

🙄

「シオンが変だ!」

これは実装にバグがあったからなのですが、原因と対処は後編でお送りします(2022/02/19 後編へのリンクを追加しました)。

動作環境

  • macOS
  • CPU環境で動かしています
  • Python 3.9.4
  • SpeechRecognition 3.8.1
  • espnet-model-zoo 0.1.7
    • espnet 0.10.6
    • torch 1.10.2
  • SoundFile 0.10.3.post1

Pythonの読み上げを聴かせて

はじめに

吉浦康裕スペース、楽しかったー!(配信ありがとうございます) nikkieです。

この記事は、アイの歌声を聴かせてに関する一技術者なりのファン活動の一幕です。

実は私、大好きなアイうたに出てくる"ポンコツAI"シオンを実装しようとしています。
今回はテキストを読み上げるやり方について、これまでに分かったことを書き留めます。
これまではmacOS限定の say コマンドで実装していましたが、機械学習モデルを使った音声合成に入門しました(レベル感としては「やってみた系」です)。

目次

背景:技書博にシオン v0.0.1実装で技術同人誌出すぞー!

放っておいたらC99通っていたけれど、書きたいものが変わっていて参加を断念した2021年末1
晦日に見た『魔女見習いをさがして』がめっちゃよく2、「アイうたファン活動として、2月の技書博6にアイうたの技術同人誌だそう」と決意。😤

ファン活動に邁進するnikkie

しかし、昨今の情勢もあって技書博は中止に😢(仕方ないけれど、残念)

私の中での気持ちの整理として、以下に落ち着きました。

  • 同人誌は無理して書かない
  • ただ、いま作りたいと思っているシオン v0.0.1は3月末までに作る(目標として掲げる)
    • 作りたいものを作らないと、自分が後悔する

そういうわけで、シオン v0.0.1実装中のアウトプットを(多分あと数回)お送りします。
今回は、テキストの読み上げ機能です。

現在のシオン v0.0.1の実装

上のツイートにあるように、スクリプトを実行すると以下のように動きます:

  1. 人が音声で入力
  2. 音声を文字に起こす
  3. 文字列を処理(今はオウム返し)
  4. 処理された文字列を読み上げる

例えば私が「こんにちは」と言うと、「こんにちは」と返します3

処理された文字列を読み上げる

これまでの実装:sayコマンド

「処理された文字列を読み上げる」のはsubprocesssayコマンドを呼び出して実装していました。

import subprocess


def call_say_command(sentence: str):  # 処理された文字列がsentenceに渡されます
    subprocess.run(["say", sentence])

これだとmacOSでは動きますが、他のOSでは動きません。
これを解決したく、機械学習モデルを使ってみることにしました。
上記を解決した先に、Dockerイメージとしてシオンを配布してどんな環境でも動かせたらと思うと、ワクワクしますね。

機械学習モデルを使った現在の実装

import sounddevice as sd
from ttslearn.dnntts import DNNTTS

dnntts_engine = DNNTTS()


def play_text_audio(sentence: str):
    audio_array, sampling_rate = dnntts_engine.tts(sentence)
    sd.play(audio_array, sampling_rate)
    sd.wait()

テキストから音声データを作る部分はttslearnというライブラリを使い、音声データの再生はsounddeviceを使っています。

ttslearn:『Pythonで学ぶ音声合成

テキストの読み上げ(Text-To-Speech:TTS)はすでに使っているsayコマンド以外にも、クラウドベンダーが提供するAPIもあります。
ですが、シオンはスタンドアローンなので、機械学習モデルを選択しました。

TTSの機械学習モデルもWeb上に大量に情報がありますが、今回扱いたいのは日本語の読み上げです。
書名だけ知っていた『Pythonで学ぶ音声合成』が最初に当たるのにぴったりではないかと思い付き、今回参考にしました。

体系だった説明もあるため、音声の扱いの経験が全然ない身には、この本に当たってよかったです。

上記のコードは、Quick startの「DNN音声合成」そのままです。
手元で動かせたので「完全に理解した!」(←分かってない)って感じです。
音声合成の理論面のキャッチアップは長い道のりですね。

sounddevice

ttslearnのコードはJupyter Notebookです。
音声データはNumPyのarray4で表されますが、Notebookではそれを再生するUIを埋め込んで対応しています。
シオンはNotebookではなく、Pythonスクリプトで実装したく、音声データ(NumPy array)の再生方法を調べました。
そこで見つかったのがsounddeviceです。

短いコードで使えるので好印象!
音声データを表すNumPyのarrayをsd.playメソッドに渡すと、音声が再生されます。
続く行でsd.wait()を呼び出すことで、音声の再生が終わるまで待ちます。

sounddeviceを知ったきっかけはRealPythonの Playing and Recording Sound in Python – Real Python です。
simpleaudioもよさそうでしたが、こちらは2021年11月にアーカイブが宣言されていました5

終わりに

シオン v0.0.1の読み上げ機能は、macOS以外の環境でも動作するようになりました!

Pythonで学ぶ音声合成』、これはとてもよさそうな本です。
この本で音声合成を学んで、まだまだシオンを改良できそうです。

アイうたの劇中では、音声合成機能がサラッと出てくるのですが、手を動かした今なら分かります、この実装は相当大変ですよ!
しかもシオンの読み上げには息の音が入りますからね。
フィクションに「魔法」っぽい部分はあっていいと思うんですが、どれくらい魔法か、手を動かして思い知りました。

変更履歴

  • 2022/07/11: 「機械学習モデルを使った現在の実装」にてFix typo(dnntts_enging → dnntts_engine)

  1. Everlasting Diary、#C99A サークル参加断念のお知らせ - nikkie-ftnextの日記

  2. 大晦日に見た『魔女見習いをさがして』が響きました - nikkie-ftnextの日記

  3. 一アイうたファンとしては、これだけでも胸熱です

  4. 今後の鑑賞でシオンが何か言うたびに、「いまNumPyのarray的なデータが裏で処理されているんだ」と一人でエモくなってそうです

  5. https://github.com/hamiltron/py-simple-audio のREADME参照

data2vecのSOTAを見せて(data2vec NLP Baseを動かしてみたログ)

はじめに

見守っていたんだよ、ずっと🎵 nikkieです。

この週末、data2vecというモデルを動かしてみました。
そのログを残します。

目次

data2vecとは

1/20にMeta AI(旧称Facebook AI)から1本のブログが公開されました。

タイトルは「The first high-performance self-supervised algorithm that works for speech, vision, and text(音声にも画像にもテキストにも有効な、初の高性能な自己教師ありアルゴリズム)」であり、data2vec という手法について紹介しています。
音声・画像・テキストというモーダルの違いによらず、データの表現を推論する点が強調されています。

同日にdata2vecの論文も公開されています。

論文のAbstractには以下のようにあります。

Models and code are available at www.github.com/pytorch/fairseq/tree/master/examples/data2vec.

「この論文、なんだかすごそう!」と気になった私は、理解の一歩目として、公開されているモデルとコードを動かしてみました。

TL; DR(この記事のサマリ)

  • fairseqのRoBERTaのドキュメントを参考に、テキスト向けのdata2vecの学習済みモデルを動かした
  • GLUEのQNLIタスクでfinetuningした
  • trainとdevデータに対する、Accuracyは67.1%, 62.4%(論文ほど高くない)

おことわり

data2vecの論文の内容については立ち入りません。
テキスト向けdata2vecのモデルを「こうやったら動かせた」というログです。

自分にとって一番とっかかりやすそうな「コードを動かす」アプローチを取っています。
誤りに気づいた場合は、コメントやTwitter @ftnext 宛にお知らせいただけますと大変ありがたいです。

fairseqとは

Facebook AI Research Sequence-to-Sequence Toolkit written in Python.

Facebook AI リサーチ1が公開している、シーケンス(=系列データ)のモデリングツールキットだそうです(READMEより)。

2/4のちゅらデータさんのイベント2でもニューラルネットワークによる校正でfairseqが紹介されていて、この週末はfairseqづくしとなりました。

fairseqのコードは実装がなかなか興味深いです(継承を使ったモデルの定義や、訓練スクリプトの引数の扱い)。

https://github.com/pytorch/fairseq#requirements-and-installation に沿って、リポジトリをcloneしてeditable installしました。

data2vecのモデルを動かすには

モデル自体はdata2vecのREADMEに記載のリンクからダウンロードできます。
今回はnlp_base.ptNLP用のベースモデル)を動かしていきます。

https://github.com/pytorch/fairseq/tree/main/examples/data2vec#training-a-new-nlp-model-with-the-cli-tools を参照すると

Please follow the RoBERTa instructions to preprocess your data.

とありました。
そこで、RoBERTa(Facebook AI リサーチによるBERTの再実装)のREADME.md https://github.com/pytorch/fairseq/blob/main/examples/roberta/README.md を参考にしています3

モデルのロードは https://github.com/pytorch/fairseq/blob/main/examples/roberta/README.md#example-usage にならいました。

>>> from fairseq.models.roberta import RobertaModel
>>> model = RobertaModel.from_pretrained(".", "nlp_base.pt")

nlp_base.ptと同じディレクトリにdict.txtが必要でした(FileNotFoundErrorが送出される)。
preprocess_GLUE_tasks.shwgetしているのを見つけ、実行してdict.txtを配置したところ、モデルがロードできました。

$ wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/dict.txt'

encode + extract_features で、ベクトルに変換できるようです。

>>> model.eval()
>>> tokens = model.encode('Hello world!')
>>> last_layer_features = model.extract_features(tokens)
>>> last_layer_features.size()
torch.Size([1, 5, 768])

Hello world!は<s>, hello, world, !, </s>に分かれて、それぞれベクトル表現になったようです。
得られたものをどう使うか、私の知識不足で、よく分かっていません(BERTのように先頭を抜き出す??)。

QNLIでfinetuning

もう少し動かしたいと、GLUE4のQNLI5というデータセットで試しました。
data2vecのモデルはタスクごとにfinetuningする必要があるという理解です。
https://github.com/pytorch/fairseq/blob/main/examples/roberta/README.glue.md を参照しています。

「1) Download the data from GLUE website」は案内されたスクリプトに記載されたリンクが403 Forbiddenとなるため、以下に置き換えました6

$ wget https://raw.githubusercontent.com/nyu-mll/GLUE-baselines/b1c82396d960fd9725517089822d15e31b9882f5/download_glue_data.py
$ python download_glue_data.py --data_dir glue_data --tasks QNLI

「2. Preprocess GLUE task data」はエラーなく終了、「3. Fine-tuning on GLUE task」は以下のコマンドで実行しています(GPUが使える環境が必要です)。

$ # pwdはfairseqリポジトリのルート
$ python fairseq_cli/hydra_train.py -m \
    --config-dir examples/roberta/config/finetuning \
    --config-name qnli \
    task.data=$PWD/QNLI-bin \
    checkpoint.restore_file=examples/data2vec/nlp_base.pt

hydraが成果物配置用ディレクトリでカレントディレクトリを上書きするという挙動のために、task.data絶対パスで指定しています。

モデルの訓練後、https://github.com/pytorch/fairseq/blob/main/examples/roberta/README.glue.md#inference-on-glue-task にあるようなスクリプトでaccuracyを求めたところ

  • train: 67.1%
  • dev: 62.4%

でした。
testはラベルが手元にないようで、accuracyは求められていません。

終わりに

Facebook AI リサーチが公開したdata2vecのモデルを動かし、finetuningしてみました。
論文の結果が再現させられませんでしたが、私の技量不足(扱うデータセットやモデルのアーキテクチャへの理解不足)が原因だと思います。
とはいえ、薄いドキュメントを辿って動かすことができたのは成果だと思っています。
ここのログを足がかりにもう少し見ていこうと思います(アップデートは追記または補足記事予定)。


  1. FacebookはMetaに社名を変えましたね。ただリポジトリやパッケージにはFacebookの名が残っていくんだろうなと思いました(いきなりmairseqには変えづらそう)

  2. 【初心者向け】Pythonで手軽に始める文章校正 - connpass YouTubehttps://youtu.be/p4w3tVE3Pg8

  3. こちらのプルリクエストでrobertaのモデルを拡張しているのも見つけたのも、RoBERTaを参考にした理由です。

  4. https://huggingface.co/datasets/glue (論文内のarXivへのリンク -> PaperWithCodeへのリンク -> データセットとスムーズにアクセスできて、高速道路感やばかったです!)

  5. 「ペアの文が質問に対する回答を含むかどうか」 ref: 2018年の言語モデル概要 - LINE ENGINEERING

  6. ref: https://github.com/pytorch/fairseq/issues/3840#issuecomment-913078184

イベントレポート | BPStudy#173〜ゼロからはじめるデータサイエンス #bpstudy

はじめに

聞いて聞いて!

リリースされた新バージョンのpipは、installのprogress barがかっこよくなってるんだよ! nikkieです。

f:id:nikkie-ftnext:20220205185009p:plain (画像は最近動かしたGitHub Actionsのログの一部のスクリーンショットです)

1/31(月)に『ゼロからはじめるデータサイエンス入門〜R・Python一挙両得〜』をテーマにしたBPStudyに参加しました。
そのレポートを綴ります。

目次

イベントの概要

今回のBPStudyは、昨年12月に出版された「ゼロからはじめるデータサイエンス入門〜R・Python一挙両得〜」の著者の矢吹 太朗さんと、辻 真吾さんに登壇いただきます。

お二人の話を伺い、データサイエンスを学ぶきっかけをつかみましょう。

みんなのPython勉強会でお世話になっている辻さんの新刊(共著)『ゼロからはじめるデータサイエンス入門』。
著者から話が聞けるということで参加しました。

1時間のプログラムは以下のような感じ:

  • イントロダクション (辻さん)
  • RとPythonの比較から見るデータサイエンス(矢吹さん)
  • データサイエンスをはじめよう!(辻さん)
  • 質疑

めちゃめちゃ面白かったです!

矢吹さんの「RとPythonの比較から見るデータサイエンス」に知的好奇心を刺激しまくられました

矢吹さん「RとPythonの比較から見るデータサイエンス」

RとPythonで同じ結果を出すための事例が6つ紹介されました。

  1. コピーと参照(03.03)
  2. 縦型と横型(03.04)
  3. 分散(04.01)
  4. ヒストグラム(04.02)
  5. 検証(07.06)
  6. 正則化(08.06)

この本に登場するコードは https://github.com/taroyabuki/fromzero で公開されています。
カッコはコードを参照するための項番号です(nikkieがレポートを書く中で振ったので、矢吹さんの発表とは違っているかもしれません)。

特に興味深かった事例を以下に示します。

事例1:コピーと参照

事例3:分散

numpy、pandasともddof(Delta Degrees of Freedom)という引数があるのですが、このデフォルト値が異なります。

事例4:ヒストグラム

終わりに

『ゼロからはじめるデータサイエンス入門』の著者から話が聞けたBPStudy#173、とても刺激的でした。

私は電子書籍を待っているのですが、リフロー版を準備中とのこと!
配信開始が楽しみですね。

参加者の皆さま、ありがとうございました。

1月の #stapy で挙がった2つの質問に回答します:キーワード専用引数はいつから? 位置専用引数の使い所は?

はじめに

秘密はね、最後に明かされるんだよ。nikkieです。

まず1スタッフとしてお礼を。
1/27(木)開催のみんなのPython勉強会、ご参加ありがとうございました。

「いろんなPythonを探検しよう」というテーマで、

  • Pythonという言語のここ5バージョンの変遷
  • パッケージングの基礎
  • NVIDIAさんの研究開発動向

と知られて、とても楽しかったです。

この記事では、laugh_kさんの登壇中に出た2つの質問を深堀ります。

  • キーワード専用引数はどのPythonバージョンから?
  • 位置専用引数はどんなときに使う?

Pythonのドキュメントを探したところ、どちらも私はちょっと驚いた答え(秘密)がありました。

目次

laugh_kさんトーク「Python3.6から3.10までのおよそ5年間の間の進化を振り返る」

laugh_kさん視点で、主観を交えて、振り返ったトークです。
PEPやWhat's Newを引きながらの振り返りで、「そうそう!」と楽しく聞いていました。
スライドを見返すだけでも楽しいと思います!

Python 3.8の振り返りの1つがPEP 570の位置専用引数(Positional-Only Parameters)です。

こちらについて、上記の2つの質問がZoomチャットやSlidoで出ました。

位置専用引数・キーワード専用引数について手短に

Pythonチュートリアルの説明を引用します。
https://docs.python.org/ja/3/tutorial/controlflow.html#special-parameters

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
    ...
  • pos1, pos2位置専用引数
    • pos1="ham" のようにキーワード引数として指定できません
  • pos_or_kwd:位置またはキーワード引数
    • 位置引数としても、キーワード引数としても指定できます
  • kwd1, kwd2キーワード専用引数
    • 位置引数として指定できません

文法上の説明としては

  • / の前までが位置専用引数
  • / の後から * の前までが、位置またはキーワード引数
  • * の後がキーワード専用引数

となります。
なお、/* はオプションであり、指定しない場合はすべての引数が位置またはキーワード引数となります。

それでは、質問に順番に答えていきます。

質問1:キーワード専用引数はどのPythonバージョンから?

laugh_kさんの振り返りにあるように、位置専用引数はPython 3.8からです。
ではキーワード専用引数はどのバージョンからでしょうか?

答えが浮かんでから読み進めるのをオススメします。


すぐに答えが見えないように、オススメの映画を宣伝しておきます。面白いからみんな観て!


私の答えは「Python 3.8」でしたが、これはブッブーでした。
Python 3.8を機に /* の説明をPythonチュートリアルで見かけるようになったと思っていたのですが、実は * (キーワード専用引数)はもっと前からあったんです。

いつからだと思いますか?

実は、キーワード専用引数は PEP 3102 で提案されているんです!

質問1への回答は Python 3.0 です。

def compare(a, b, *, key=None):というコードに以下の説明がされています:

The second syntactical change is to allow the argument name to be omitted for a varargs argument.

*argsのような可変長位置引数の変数名を削って * としたようです。

私はPython 3.6から触り始めたのですが、「入門時にはキーワード専用引数はあったんだなー」と分かって、Python 3.8まで知らなかったことに驚きました。

質問2:位置専用引数はどんなときに使う?

振り返りで登場した「位置専用引数」の利用シーンの質問です。

Effective Python 第2版』で読んだ記憶があったので、勉強会中に以下を共有しました。

「Effective Pythonにしか書かれていないのかな」と気になり、Pythonのドキュメントの中を探したところ、質問への回答がありました。

Pythonチュートリアルにあった「要約」

https://docs.python.org/ja/3/tutorial/controlflow.html#recap

  • もし引数の名前をユーザーに知らせる必要がないなら、位置専用引数を使用しましょう。
  • APIの場合、将来引数の名前が変更された場合にAPIの変更ができなくなることを防ぐために、位置専用引数を使用しましょう。

利用シーンは、ユーザが引数の名前を知らなくてもいいときや引数の名前を変更する可能性があるときということですね。
この要約にはキーワード専用引数についても載っています。

What's New in Python 3.8

https://docs.python.org/ja/3.8/whatsnew/3.8.html#positional-only-parameters

One use case for this notation is that it allows pure Python functions to fully emulate behaviors of existing C coded functions.

(意訳気味:この記法の1つのユースケースは、純粋なPythonの関数が既存のCのコードの関数と完全に同じ振る舞いをするのを可能にすることです)

もう一つのユースケースは、引数名が有用ではない場合にキーワード引数としての利用を排除することです。(略)これにより、以下のようなぎこちない呼び出しが排除されます:

チュートリアル同様に、引数名変更のメリットも書かれています。

引数を位置専用とすることは、のちに引数名を変更する際に呼び出し側のコードを壊してしまう心配がないという利点があります。

まとめ

冒頭の質問への回答をまとめます。

  • キーワード専用引数はどのPythonバージョンから? 👉 Python 3.0
    • PEP 3102で導入された
  • 位置専用引数はどんなときに使う? 👉 以下に抜粋
    • ユーザが引数の名前を知らなくてもいいとき
    • 引数名が有用でない場合(キーワード引数として使えなくすることで、可読性を上げる)
    • APIの場合(将来引数名を変更できるようにする)

位置専用引数の利用シーンが分かったので、普段の開発で積極的に使っていけそうです。
私が書くコードには、使いどころが結構あるように感じています。