Emacsで補完入力を行うCompany-modeの設定 2021

はじめに

Emacsで補完入力を行うためのパッケージとしてcompany-modeがよく知られている。
本記事ではこの設定を紹介し、備忘録として残しておく。

設定

以下の設定を適用する。

コードを表示する

;; -*- mode: emacs-lisp; coding: utf-8

;; 主なデフォルトのキーバインド
;;
;; M-n 次の候補選択
;; M-p 前の候補選択
;; C-g 候補選択中止
;; RET 候補選択
;; F1, C-h docを別バッファに表示
;; C-w 定義場所を表示 (関数)
;; C-s 候補検索; 表示されている範囲で入力クエリにより候補選択 (modelineが変化する)
;; C-M-s 候補フィルタリング
;; [tab] 共通するprefixを挿入

;; C-sやC-M-sは忘れがちだが使うと便利

(require 'company)
(add-hook 'after-init-hook 'global-company-mode)

(setq company-idle-delay 0.3)
(setq company-minimum-prefix-length 2)

;; キー設定
(define-key company-active-map (kbd "C-n") 'company-select-next)
(define-key company-active-map (kbd "C-p") 'company-select-previous)
(define-key company-active-map (kbd "M-n") nil)
(define-key company-active-map (kbd "M-p") nil)
(define-key company-active-map (kbd "C-m") 'company-complete-selection)

;; 候補に番号を付け、
;; M-1やM-2などM-をつけて押すことで候補選択をショートカットできる
(setq company-show-numbers t)

;; 候補の一番下でさらに下に行こうとすると一番上に戻る
(setq company-selection-wrap-around t)

;; ツールチップ上の候補を右揃え
(setq company-tooltip-align-annotations t)

;; 参考 https://qiita.com/zk_phi/items/9dc373e734d20cd31641
;; シンボルの途中部分を補完する
;; → company にはこの挙動が実装されておらず、カーソルの直後に (空白以外
;; の) 文字がある場合は補完が始まらないため
(require 'company-anywhere)

;; 参考 https://qiita.com/zk_phi/items/9dc373e734d20cd31641
;; ac-dwim時の挙動をcompanyで再現
(require 'company-dwim)
(define-key company-active-map (kbd "TAB") 'company-dwim)
(setq company-frontends
      '(company-pseudo-tooltip-unless-just-one-frontend
        company-dwim-frontend
        company-echo-metadata-frontend))

;; 参考 https://qiita.com/zk_phi/items/9dc373e734d20cd31641
;; company-require-match
;; - never: 補完候補の選択を開始後、補完候補にない文字も打てる
(setq company-require-match 'never)

;; 参考 https://qiita.com/zk_phi/items/9dc373e734d20cd31641
;; 『これは「現在編集中のバッファ、またはそのバッファと同じメジャーモー
;; ドになっているバッファ」から補完候補を探してきてくれるものです。』
(require 'company-same-mode-buffers)
(company-same-mode-buffers-initialize)
(setq company-backends
      `(
        company-dabbrev
        company-capf
        company-semantic
        company-cmake
        company-clang
        company-files

        ;; https://qiita.com/zk_phi/items/9dc373e734d20cd31641
        ;; 「組み込みのキーワードも words-in-same-mode-buffers
        ;; の候補もみんないっしょくたに出てくる」
        ;; (company-keywords :with company-same-mode-buffers)

        (company-dabbrev-code company-gtags company-etags
                              company-keywords)
        company-oddmuse
        company-bbdb
        ,@(unless (version< "24.3.51" emacs-version)
            (list 'company-elisp))
        ,@(unless (version<= "26" emacs-version)
            (list 'company-nxml))
        ,@(unless (version<= "26" emacs-version)
            (list 'company-css))
        ))
(push 'company-same-mode-buffers company-backends) ;先頭に追加

;; lowercaseのみで補完されるのを抑制する(case sensitive)
(eval-when-compile (require 'company-dabbrev))
(setq company-dabbrev-downcase nil)

;; 参考 https://shwaka.github.io/2017/05/03/company-dabbrev-japanese.html
;; company-dabbrevで日本語文字が補完候補に含まれないようにする
(defun edit-category-table-for-company-dabbrev (&optional table)
  (define-category ?s "word constituents for company-dabbrev" table)
  (let ((i 0))
    (while (< i 128)
      (if (equal ?w (char-syntax i))
          (modify-category-entry i ?s table)
        (modify-category-entry i ?s table t))
      (setq i (1+ i)))))
(edit-category-table-for-company-dabbrev)
(setq company-dabbrev-char-regexp "\\cs")

;; company-same-mode-buffers.elにおいて "\\sw"が使用されている箇所を "\\cs"に変更する
;; →日本語文字が補完候補に含まれることを抑制する効果
(defun company-same-mode-buffers-query-construct-any-followed-by (str)
  (cons "\\(?:\\cs\\|\\s_\\)*?" (regexp-quote str)))
(defun company-same-mode-buffers-query-construct-any-word-followed-by (str)
  (cons "\\cs*?" (regexp-quote str)))
(defun company-same-mode-buffers-query-to-regex (lst)
  "Make a regex from a query (list of `query-construct-*' s)."
  (concat "\\_<"
          (mapconcat (lambda (pair) (concat (car pair) "\\(" (cdr pair) "\\)")) lst "")
          "\\(?:\\s_\\|\\cs\\)*"))
(defun company-same-mode-buffers-update-cache (&optional buffer)
  "Put all symbols in the buffer into
`company-same-mode-buffers-cache'."
  (with-current-buffer (or buffer (current-buffer))
    (when (and company-same-mode-buffers-cache-is-dirty
               (derived-mode-p 'prog-mode))
      (let ((tree (gethash major-mode company-same-mode-buffers-cache))
            (symbols (company-same-mode-buffers-search-current-buffer
                      (concat "\\(:?+\\cs\\|\\s_\\)\\{"
                              (number-to-string company-same-mode-buffers-minimum-word-length)
                              ","
                              (number-to-string company-same-mode-buffers-maximum-word-length)
                              "\\}"))))
        (dolist (s symbols)
          (setq tree (company-same-mode-buffers-tree-insert tree s)))
        (puthash major-mode tree company-same-mode-buffers-cache)
        (setq company-same-mode-buffers-cache-is-dirty nil)))))

;; 参考 https://qiita.com/sune2/items/b73037f9e85962f5afb7#company-transformers
;; 補完候補のソート。nilが辞書順
(setq company-transformers
      '(company-sort-by-occurrence company-sort-by-backend-importance))

;; prescientのアルゴリズム
;; - 直近の選択を上位に移すソーティング
;; - 直近の選択を履歴として保存する
(require 'company-prescient)
(company-prescient-mode 1)
;; →company-transformersに prescient由来のものを追加

;; companyで英単語を補完するためのcompany-wordfreq →MELPAからダウンロード
;; 専用の単語辞書をダウンロードするため以下のコマンドを実行しておく
;; M-x company-wordfreq-download-list
(add-hook 'text-mode-hook (lambda ()
                            (setq-local company-backends
                                        '(company-wordfreq
                                          company-dabbrev
                                          company-capf))
                            (setq-local company-transformers nil)))

;; irony C/C++
(require 'company-irony-c-headers)
(setq company-backends (append company-backends '(company-irony-c-headers)))
(setq company-backends (append company-backends '(company-irony)))
(setq company-backends (append company-backends '(company-reftex-labels)))
(setq company-backends (append company-backends '(company-reftex-citations)))

(provide 'init-company)


補足

zk_phi氏の以下の記事を参考にした部分は多い。
qiita.com
同氏の記事では候補のソーティングにcompany-statisticsが使われていたが、本記事ではcompany-prescientを採用した。

また、company-dabbrevで日本語文字が補完候補に含まれないようにするための設定では以下の記事を参考にした。
shwaka.github.io
この記事で導入された正規表現”\\\cs”をzk_phi氏謹製のcompany-same-mode-buffers.elにも適用し、日本語文字の候補出現を抑制する。このため一部の関数を再定義した。

英単語をcompanyで補完するために、company-wordfreqの設定も入れてある。


おわりに

ひとまず快適に使えているので良しとする。

EmacsからJupyter Notebookを触るための設定

はじめに

Emacsのeinというパッケージを用いることで、EmacsからJupyter Notebookを編集し、表示することができる。
実行イメージは以下の通りである。

Emacsから直接Jupyter Notebookが編集できるので、ブラウザにはない各種補完機能やカーソルジャンプ、undoなど充実したEmacsの編集機能を継承して使えるので便利ということである。

本記事ではその設定をメモしておく。

事前準備

1. jupyterをインストール

pip3 install jupyter

2. MELPAからeinをインストール(必須)

M-x package-install RET ein RET

設定

(eval-when-compile
  (require 'ein)
  (require 'ein-notebook)
  (require 'ein-notebooklist)
  (require 'ein-markdown-mode)
  (require 'smartrep))

;; (add-hook 'ein:notebook-mode-hook 'electric-pair-mode) ;; お好みで
;; (add-hook 'ein:notebook-mode-hook 'undo-tree-mode) ;; お好みで

;; undoを有効化 (customizeから設定しておいたほうが良さげ)
(setq ein:worksheet-enable-undo t)

;; 画像をインライン表示 (customizeから設定しておいたほうが良さげ)
(setq ein:output-area-inlined-images t)

;; markdownパーサー
;; M-x ein:markdown →HTMLに翻訳した結果を*markdown-output*バッファに出力
(require 'ein-markdown-mode)

;; pandocと markdownコマンドは入れておく
;; brew install pandoc
;; brew install markdown
(setq ein:markdown-command "pandoc --metadata pagetitle=\"markdown preview\" -f markdown -c ~/.pandoc/github-markdown.css -s --self-contained --mathjax=https://raw.githubusercontent.com/ustasb/dotfiles/b54b8f502eb94d6146c2a02bfc62ebda72b91035/pandoc/mathjax.js")

;; markdownをhtmlに出力してブラウザでプレビュー
(defun ein:markdown-preview ()
  (interactive)
  (ein:markdown-standalone)
  (browse-url-of-buffer ein:markdown-output-buffer-name))

;; smartrepを入れておく。
;; C-c C-n C-n C-n ... で下のセルに連続で移動、
;; その途中でC-p C-p C-pで上のセルに連続で移動など
;; セル間の移動がスムーズになってとても便利
(declare-function smartrep-define-key "smartrep")
(with-eval-after-load "ein-notebook"
  (smartrep-define-key ein:notebook-mode-map "C-c"
    '(("C-n" . 'ein:worksheet-goto-next-input-km)
      ("C-p" . 'ein:worksheet-goto-prev-input-km))))

実行手順

1. M-x ein:run を実行
Notebookを作成したいディレクトリを選択できるようになる。ディレクトリを決定するとJupyterがバックグラウンドで起動する。

2. カーネルを選択
要するにPythonのバージョン指定。

3. ファイル/ノートブック一覧から、ノートブックのところの[Open]をRET
既存のNotebookが起動する。

4. Notebook新規作成は[New Notebook]をRET

使い方

主なキーバインドとコマンド一覧を以下に示す。

とりあえず、セルに何か書いたらC-c C-cすれば、セルが評価される。

キーバインド/コマンド 説明
M-x ein:run jupyterを起動
C-c C-t 当該セルの評価モードをPython->MarkDown->rawの順に切り替える
C-c C-n 1つ下のセルに移動
C-c C-p 1つ上のセルに移動
C-c C-e 当該セルの表示をトグル
C-c C-c 当該セルを評価(Pythonのみ; Markdownは不可) →C-uをつけて実行するとすべてのセルを先頭から評価できる
C-c C-k 当該セルを削除
C-c C-a 新規セルを当該セルの上に作成 (C-uを先につけるとMarkdownで作成)
C-c C-b 新規セルを当該セルの下に作成 (C-uを先につけるとMarkdownで作成)
C-c C-s 当該セルを分割(split)
C-c C-m 当該セルをマージ(merge)
C-c C-l 当該セルの出力をクリア
C-c C-S-l 全てのセルの出力をクリア
C-x C-s 現在のnotebookを保存
C-x C-w 現在のnotebookを別名保存
M-x ein:notebook-save-to-command 当該ノートのコピーを、名前を付けて保存
C-c C-x C-r notebookのセッションを再起動する →番号をクリアしたいときに使う
M-x ein:stop jupyterを終了

Markdownのプレビューには、設定ファイルに書いた ein:markdown-preview を使うことでブラウザ上のプレビューが可能である。応用すればewwやemacs-w3m上で表示させ、Emacs内で完結させることもできるだろう。

おまけ:Einにおける数式表示

Markdown中にLatexで記述した数式をプレビューするための設定。インライン表示ではない。
実行イメージはこちら:


設定

M-x my-markdown-preview-latexとするとバッファがポップアップして、そこに数式が表示される。

(defun my-markdown-preview-latex ()
  "Preview LaTeX from the current cell in a separate buffer.

Handles only markdown and code cells, but both in a bit different
ways: on the former, its input is being rendered, while on the
latter - its output."
  (interactive)
  (let* ((cell (ein:worksheet-get-current-cell))
         (text-to-render
          (cond ((ein:markdowncell-p cell) (slot-value cell :input))
                ((ein:codecell-p cell)
                 (plist-get (car (cl-remove-if-not
                                  (lambda (e) (string= (plist-get e :name) "stdout"))
                                  (slot-value cell :outputs)))
                            :text))
                (t (error "Unsupported cell type"))))
         (buffer (get-buffer-create " *ein: LaTeX preview*")))
    (with-current-buffer buffer
      (when buffer-read-only
        (read-only-mode -1))
      (unless (= (point-min) (point-max))
        (delete-region (point-min) (point-max)))
      (insert text-to-render)
      (goto-char (point-min))
      (org-mode)
      (org-toggle-latex-fragment 16)
      (special-mode)
      (unless buffer-read-only
        (read-only-mode 1))
      (display-buffer
       buffer
       '((display-buffer-below-selected display-buffer-at-bottom)
         (inhibit-same-window . t)))
      (fit-window-to-buffer (window-in-direction 'below)))))

おわりに

EmacsからJupyter Notebook、一度試してみる価値はあると思う。

EmacsでPythonを書く設定 2021

はじめに

PythonまわりのEmacsの設定を整理したということ。Company-modeの設定は言語共通の部分が多く長くなるので省略した。Language serverの紹介がメイン。

※最新の設定は2022の記事に

tam5917.hatenablog.com

Language server

Emacsからlanguage serverを使うためにeglotを入れる。MELPAからインストール可能。さらにPython用のpython-language-serverをインストールしておく。ターミナルから以下を実行する。

pip3 install python-language-server

関連パッケージをまとめて入れる場合は以下がラク

pip install 'python-language-server[all]'

Flymake

flycheckはOFFにして、flymakeを使うようにした。flymake-diagnostic-at-pointはなかなか便利なのでおすすめ。MELPAからインストール可能。

python-black

Emacsからblackを呼び出せるようにするため、python-blackを導入する。
https://github.com/wbolster/emacs-python-black

バッファ保存時に「常に」blackをかけたいときは、以下のように設定可能である。

(declare-function python-black-on-save-mode "python-black")
(add-hook 'python-mode-hook
          #'(lambda ()
              (python-black-on-save-mode)))

python-black-on-save-modeを持ち出さない場合は、以下でもOK。

(add-hook 'python-mode-hook
          #'(lambda ()
              (add-hook 'before-save-hook
                        'python-black-buffer nil t)))

前者のminor-modeを使うとlighterの文字列が増えてゴチャゴチャするので、個人的には後者が好み。
projectがblackによるフォーマットを必要とするかに応じて振る舞いを変えたい場合はpython-black-on-save-mode-enable-dwimを使う。このようにユーザに選択の余地が残されているのがよい。

設定

;; Python
(setq auto-mode-alist (cons '("\\.py$" . python-mode) auto-mode-alist))
(setq interpreter-mode-alist (cons '("python" . python-mode)
                                   interpreter-mode-alist))

;; pip3 install python-language-server
;; https://github.com/palantir/python-language-server
;;
;; 以下は追加で(お好みで)インストール
;; pip install 'python-language-server[all]' としてしまうのが早い
;; 
;; pip3 install rope ; completions and renaming
;; pip3 install pyflakes ; linter to detect various errors
;; pip3 install mccabe ; linter for complexity checking
;; pip3 install pycodestyle ; linter for style checking
;; pip3 install pydocstyle ; linter for docstring style checking
;; pip3 install pyls-mypy ; type checker (heavy)
;; pip3 install pyls-isort ; import sort code formatting
;; pip3 install pyls-black ; code formatting using Black

(require 'eglot)
(add-hook 'python-mode-hook 'eglot-ensure)

;; server として起動
;; (add-to-list 'eglot-server-programs
;;              `(python-mode . ("pyls" "-v" "--tcp" "--host"
;;                               "localhost" "--port" :autoport)))

;; 保存時に自動整形 (black)
;; python-blackをmelpaからインストールしておく
(add-hook 'python-mode-hook
          #'(lambda ()
              (add-hook 'before-save-hook
                        'python-black-buffer nil t)))

;; 以下の設定でもOKだが、minor-modeの分、lighterが増えてごちゃごちゃする
;; (declare-function python-black-on-save-mode "python-black")
;; (add-hook 'python-mode-hook
;;           #'(lambda ()
;;               (python-black-on-save-mode)))

;; 保存時にインポート順を自動修正
;; py-isortをmelpaからインストールしておく
(add-hook 'python-mode-hook #'(lambda ()
                                (add-hook 'before-save-hook
                                          'py-isort-buffer nil t)))

;; point位置にメッセージを表示する。eldocと干渉しないのが利点
(with-eval-after-load "flymake"
  (require 'flymake-diagnostic-at-point)
  (add-hook 'flymake-mode-hook #'flymake-diagnostic-at-point-mode))

その他

ほか、以下は好みで設定した。エコーエリアが複数行になるのを防ぐ効果がある。

(custom-set-variables
  '(eldoc-echo-area-use-multiline-p nil))

またsmart-jumpやivy-xrefも便利なので使っている。

(require 'smart-jump)
(smart-jump-setup-default-registers)  ;; pythonはデフォルトで有効になる
;; M-. (smart-jump-go; xref-find-definitions)
;; M-, (smart-jump-back; xref-pop-marker-stack)
;; M-? (smart-jump-references; xref-find-references)

(require 'ivy-xref)
(setq xref-show-xrefs-function #'ivy-xref-show-xrefs)

もし、blackユーザならばpycodestyleの設定をしておくと良い。

[pycodestyle]
ignore = E203, W503, W504
max-line-length = 88

おわりに

eglotとpython-language-serverの組み合わせがなかなか快適だったので紹介した。lsp-modeを設定するのも悪くはないし、elpyとflycheckの組み合わせで書くのも悪くはないので(特に軽快性が優れる)、このあたりは好みだろう。

Griffin-Limアルゴリズムの実行時間をlibrosaとtorchaudioで比較してみた話

はじめに

音声の振幅スペクトルから位相を推定し、元の音声を復元するためのGriffin-Limアルゴリズムが知られている。
Griffin-Limアルゴリズムはlibrosaパッケージとtorchaudioパッケージの両方に実装されている。

  • librosa

librosa.org

  • torchaudio

pytorch.org

今回はGriffin-Limアルゴリズムに関してCPUとGPUとで実行時間を計測し比較してみた。

環境

  • OS、ハードウェア、ドライバ
  • ソフトウェア
    • Python 3.6.9
    • librosa 0.8.0
    • torch 1.7.1
    • torchaudio 0.7.2
    • torchlibrosa 0.0.9

スクリプト

"""
PyTorch script for verification of execution time.
"""

# Commentary:
# torch 1.8.1
# torchaudio 0.8.1
# torchlibrosa 0.0.9

# GeForce RTX 2070
# CUDA 11.2
# CUDA Driver 460.32.03

import time

import librosa
import numpy as np
import torch
import torchaudio
import torchaudio.transforms as T

N_FFT = 1024
WIN_LENGTH = None
HOP_LENGTH = 512
N_ITER = 50

if __name__ == "__main__":

    waveform, sample_rate = librosa.load(librosa.ex("trumpet"))

    # librosa
    start = time.time()
    specgram = np.abs(
        librosa.stft(
            waveform, n_fft=N_FFT, hop_length=HOP_LENGTH, win_length=WIN_LENGTH
        )
    )
    y_inv = librosa.griffinlim(
        specgram, n_iter=N_ITER, hop_length=HOP_LENGTH, win_length=WIN_LENGTH
    )
    elapsed_time = time.time() - start
    print("elapsed_time (librosa): {0:.6f}".format(elapsed_time) + "[sec]")

    # load waveform as torch.Tensor
    waveform, sample_rate = torchaudio.load(librosa.ex("trumpet"))
    waveform_cuda = waveform.cuda()

    # torchaudio (CPU)
    specgram = T.Spectrogram(
        n_fft=N_FFT,
        win_length=WIN_LENGTH,
        hop_length=HOP_LENGTH,
    )
    griffin_lim = T.GriffinLim(
        n_fft=N_FFT,
        n_iter=N_ITER,
        win_length=WIN_LENGTH,
        hop_length=HOP_LENGTH,
    )
    start = time.time()
    reconstructed = griffin_lim(specgram(waveform))
    elapsed_time = time.time() - start
    print("elapsed_time (torchaudio; CPU): {0:.6f}".format(elapsed_time) + "[sec]")

    # torchaudio (GPU)
    specgram = T.Spectrogram(
        n_fft=N_FFT,
        win_length=WIN_LENGTH,
        hop_length=HOP_LENGTH,
    ).cuda()
    griffin_lim = T.GriffinLim(
        n_fft=N_FFT,
        n_iter=N_ITER,
        win_length=WIN_LENGTH,
        hop_length=HOP_LENGTH,
    ).cuda()
    torch.cuda.synchronize()
    start = time.time()
    reconstructed = griffin_lim(specgram(waveform_cuda))
    torch.cuda.synchronize()
    elapsed_time = time.time() - start
    print("elapsed_time (torchaudio; GPU): {0:.6f}".format(elapsed_time) + "[sec]")

実行結果

GPUを使ったtorchaudioが一番早い結果となった。

elapsed_time (librosa): 0.264543[sec]
elapsed_time (torchaudio; CPU): 0.147203[sec]
elapsed_time (torchaudio; GPU): 0.022305[sec]

まとめ

位相復元に関しても、torchaudioのCPU版を使うだけで実行時間の短縮が望めそうである。GPU版も悪くない結果である。今回は内部的に実装されているアルゴリズムを考慮したフェアな比較というよりは、いちユーザが関数を外から触るときの体感を重視した。

音声認識結果を音声合成するPythonスクリプトをSpeechRecognitionとPyOpenJTalkで書いたみた話

はじめに

かつて、音声認識音声合成を組み合わせて遊んでみるという主旨の記事を書いたことがある。

tam5917.hatenablog.com

音声合成には、コマンドラインから音声合成できるOpenJTalkパッケージを用いたのだった。これをPythonから動かす場合には、専用の関数を定義して呼び出す必要があり、ひと手間かかる。

最近、山本りゅういち氏がPyOpenJTalkなるパッケージを整備し、単体でTTS(Text-to-Speech、テキストを音声に変換する)機能を実現した。

これを使って前回記事のPythonスクリプトを少しきれいにしたというのが本記事の主旨である。

インストール

音声認識にはSpeechRecognitionパッケージを使う。PyOpenJTalkが今回のキモなので忘れずにインストールする。最新版をインストールするという意味で--upgradeオプションもつけておく。

pip3 install SpeechRecognition
pip3 install --upgrade pyopenjtalk

スクリプト

以下のスクリプトを適当な名前で保存し、実行する。 「話しかけてみましょう!」と表示されたのち、適当な言葉をしゃべると、認識された言葉をオウム返しで音声合成して終了するというシンプルなものである。

import subprocess

import numpy as np
import pyopenjtalk
import speech_recognition as sr
from scipy.io import wavfile

OUT_WAV = "/tmp/tts.wav"

if __name__ == "__main__":

    # マイクからの音声入力
    r = sr.Recognizer()
    with sr.Microphone() as source:
        print("話しかけてみましょう!")
        audio = r.listen(source)

    try:
        # 日本語でGoogle音声認識
        text = r.recognize_google(audio, language="ja-JP")
    except sr.UnknownValueError:
        print("Google音声認識は音声を理解できませんでした。")
    except sr.RequestError as e:
        print("Google音声認識サービスからの結果を要求できませんでした;" " {0}".format(e))
    else:
        print(text)

        # 音声認識結果を音声合成する
        x, sr = pyopenjtalk.tts(text)

        # wavファイルに保存
        wavfile.write(OUT_WAV, sr, x.astype(np.int16))

        # 保存したwavファイルを再生
        subprocess.run("afplay " + OUT_WAV, shell=True)

おわりに

PyOpenJTalkのおかげでTTSが手軽に実現でき、楽しい。PyOpenJTalkには話速変換やピッチ変換のオプションもあるので、調べて使ってみるのが良いだろう。

おまけ

話速変換とピッチ変換のオプションを使ったnotebookはこちらから。 nbviewer.jupyter.org

注意点

実行時のエラー

File "pyopenjtalk/htsengine.pyx", line 1, in init pyopenjtalk.htsengine
ValueError: numpy.ndarray size changed, may indicate binary incompatibility. Expected 88 from C header, got 80 from PyObject

このようなエラーが出たら、numpyを再インストールしてみるのが吉。

Google音声認識API

ただし、本記事で利用しているGoogle音声認識APIはあくまでテスト/デモ用であり、いつGoogle側にAPI利用を打ち切られても文句は言えない。Google製の音声認識APIを使いたい場合はCould Speech-to-Textの認証情報を取得して使うのが本筋である。ひと月あたり60分の音声までなら利用無料であるが、それ以上を利用するにはお金がかかる。

cloud.google.com

torchaudioとtorchlibrosaの実行速度に違いはあるのか?

はじめに

PyTorchには音声系データを処理するのに便利なtorchaudioというライブラリが存在する。
pytorch.org

一方、音声系データの処理に便利なlibrosaというパッケージが存在する。
librosa.org

さらにtorchlibrosaという、librosa内部の行列計算まわりをPyTorchで置き換えたパッケージが存在する。
github.com

ここで一つ疑問:
「で、結局どれ使えばいいの?(どれが早いの?)」

この疑問が気になったので、CPU実行とGPU実行における実行時間を比較検証するための簡易的なスクリプトを書いて調べてみたということである。

環境

  • OS、ハードウェア、ドライバ
  • ソフトウェア
    • Python 3.6.9
    • librosa 0.8.0
    • torch 1.7.1
    • torchaudio 0.7.2
    • torchlibrosa 0.0.9

スクリプト

メルスペクトログラムを計算するスクリプトである。

  • 3月24日追記:GPUの計測方法に誤りがあったため、コメントに従ってtorch.cuda.synchronize()を用いるように修正した。

スクリプトを表示する

"""
PyTorch script for verification of execution time.
"""

# Commentary:
# torch 1.7.1
# torchaudio 0.7.2
# torchlibrosa 0.0.9

# GeForce RTX 2070
# CUDA 11.2
# CUDA Driver 460.32.03

import time

import librosa
import librosa.feature
import torch
import torchaudio
import torchaudio.transforms as T
import torchlibrosa as tl

N_FFT = 1024
WIN_LENGTH = None
HOP_LENGTH = 512
N_MELS = 128

if __name__ == "__main__":

    waveform, sample_rate = librosa.load(librosa.ex("nutcracker"))
    # librosa
    start = time.time()
    mel_spectrogram = librosa.feature.melspectrogram(
        y=waveform,
        sr=sample_rate,
        n_fft=N_FFT,
        hop_length=HOP_LENGTH,
        n_mels=N_MELS,
        power=2.0,
    )
    elapsed_time = time.time() - start
    print("elapsed_time (librosa): {0:.6f}".format(elapsed_time) + "[sec]")

    waveform, sample_rate = torchaudio.load(librosa.ex("nutcracker"))
    waveform_cuda = waveform.cuda()

    # torchaudio (CPU)
    start = time.time()
    mel_spectrogram = T.MelSpectrogram(
        sample_rate=sample_rate,
        n_fft=N_FFT,
        win_length=WIN_LENGTH,
        hop_length=HOP_LENGTH,
        power=2.0,
        n_mels=N_MELS,
    )
    melspec = mel_spectrogram(waveform)
    elapsed_time = time.time() - start
    print("elapsed_time (torchaudio; CPU): {0:.6f}".format(elapsed_time) + "[sec]")

    # torchaudio (GPU)
    torch.cuda.synchronize()
    start = time.time()
    mel_spectrogram = T.MelSpectrogram(
        sample_rate=sample_rate,
        n_fft=N_FFT,
        win_length=WIN_LENGTH,
        hop_length=HOP_LENGTH,
        power=2.0,
        n_mels=N_MELS,
    )
    mel_spectrogram = mel_spectrogram.cuda()
    melspec = mel_spectrogram(waveform_cuda)
    torch.cuda.synchronize()
    elapsed_time = time.time() - start
    print("elapsed_time (torchaudio; GPU): {0:.6f}".format(elapsed_time) + "[sec]")

    # torchlibrosa (CPU)
    # TorchLibrosa feature extractor the same as librosa.feature.melspectrogram()
    start = time.time()
    feature_extractor = torch.nn.Sequential(
        tl.Spectrogram(
            n_fft=N_FFT,
            win_length=WIN_LENGTH,
            hop_length=HOP_LENGTH,
            power=2.0,
        ),
        tl.LogmelFilterBank(
            n_fft=N_FFT,
            sr=sample_rate,
            n_mels=N_MELS,
            is_log=False,  # Default is true
        ),
    )
    logmel = feature_extractor(waveform)
    elapsed_time = time.time() - start
    print("elapsed_time (torchlibrosa; CPU): {0:.6f}".format(elapsed_time) + "[sec]")

    # torchlibrosa (GPU)
    # TorchLibrosa feature extractor the same as librosa.feature.melspectrogram()
    torch.cuda.synchronize()
    start = time.time()
    feature_extractor = torch.nn.Sequential(
        tl.Spectrogram(
            n_fft=N_FFT,
            win_length=WIN_LENGTH,
            hop_length=HOP_LENGTH,
            power=2.0,
        ),
        tl.LogmelFilterBank(
            n_fft=N_FFT,
            sr=sample_rate,
            n_mels=N_MELS,
            is_log=False,  # Default is true
        ),
    )
    feature_extractor = feature_extractor.cuda()
    logmel = feature_extractor(waveform_cuda)
    torch.cuda.synchronize()
    elapsed_time = time.time() - start
    print("elapsed_time (torchlibrosa; GPU): {0:.6f}".format(elapsed_time) + "[sec]")


  • 5月8日追記:koukyo1213様のコメントに従い、torch.nn.Sequentialやtorchaudio.transforms.MelSpectrogramのインスタンス化の部分を計測対象から外し、計算処理のみにフォーカスしたスクリプトは以下である。

スクリプトを表示する

"""
PyTorch script for verification of execution time.
"""

# Commentary:
# torch 1.8.1
# torchaudio 0.8.1
# torchlibrosa 0.0.9

# GeForce RTX 2070
# CUDA 11.2
# CUDA Driver 460.32.03

import time

import librosa
import librosa.feature
import torch
import torchaudio
import torchaudio.transforms as T
import torchlibrosa as tl

N_FFT = 1024
WIN_LENGTH = None
HOP_LENGTH = 512
N_MELS = 128

if __name__ == "__main__":

    torchaudio.set_audio_backend("sox_io")

    waveform, sample_rate = librosa.load(librosa.ex("nutcracker"))
    # librosa
    start = time.time()
    mel_spectrogram = librosa.feature.melspectrogram(
        y=waveform,
        sr=sample_rate,
        n_fft=N_FFT,
        hop_length=HOP_LENGTH,
        n_mels=N_MELS,
        power=2.0,
    )
    elapsed_time = time.time() - start
    print("elapsed_time (librosa): {0:.6f}".format(elapsed_time) + "[sec]")

    waveform, sample_rate = torchaudio.load(librosa.ex("nutcracker"))
    waveform_cuda = waveform.cuda()

    # torchaudio (cpu)
    mel_spectrogram = T.MelSpectrogram(
        sample_rate=sample_rate,
        n_fft=N_FFT,
        win_length=WIN_LENGTH,
        hop_length=HOP_LENGTH,
        power=2.0,
        n_mels=N_MELS,
    )
    start = time.time()
    melspec = mel_spectrogram(waveform)
    elapsed_time = time.time() - start
    print("elapsed_time (torchaudio; CPU): {0:.6f}".format(elapsed_time) + "[sec]")

    # torchaudio (GPU)
    mel_spectrogram = T.MelSpectrogram(
        sample_rate=sample_rate,
        n_fft=N_FFT,
        win_length=WIN_LENGTH,
        hop_length=HOP_LENGTH,
        power=2.0,
        n_mels=N_MELS,
    )
    torch.cuda.synchronize()
    start = time.time()
    mel_spectrogram = mel_spectrogram.cuda()
    melspec = mel_spectrogram(waveform_cuda)
    torch.cuda.synchronize()
    elapsed_time = time.time() - start
    print("elapsed_time (torchaudio; GPU): {0:.6f}".format(elapsed_time) + "[sec]")

    # torchlibrosa (CPU)
    # TorchLibrosa feature extractor the same as librosa.feature.melspectrogram()
    feature_extractor = torch.nn.Sequential(
        tl.Spectrogram(
            n_fft=N_FFT,
            win_length=WIN_LENGTH,
            hop_length=HOP_LENGTH,
            power=2.0,
        ),
        tl.LogmelFilterBank(
            n_fft=N_FFT,
            sr=sample_rate,
            n_mels=N_MELS,
            is_log=False,  # Default is true
        ),
    )
    start = time.time()
    logmel = feature_extractor(waveform)
    elapsed_time = time.time() - start
    print("elapsed_time (torchlibrosa; CPU): {0:.6f}".format(elapsed_time) + "[sec]")

    # torchlibrosa (GPU)
    # TorchLibrosa feature extractor the same as librosa.feature.melspectrogram()
    feature_extractor = torch.nn.Sequential(
        tl.Spectrogram(
            n_fft=N_FFT,
            win_length=WIN_LENGTH,
            hop_length=HOP_LENGTH,
            power=2.0,
        ),
        tl.LogmelFilterBank(
            n_fft=N_FFT,
            sr=sample_rate,
            n_mels=N_MELS,
            is_log=False,  # Default is true
        ),
    )
    torch.cuda.synchronize()
    start = time.time()
    feature_extractor = feature_extractor.cuda()
    logmel = feature_extractor(waveform_cuda)
    torch.cuda.synchronize()
    elapsed_time = time.time() - start
    print("elapsed_time (torchlibrosa; GPU): {0:.6f}".format(elapsed_time) + "[sec]")


実行結果

その1(3月24日版)

GPUを使ったtorchaudioが一番早い結果となった。

elapsed_time (librosa): 0.042655[sec]
elapsed_time (torchaudio; CPU): 0.021293[sec]
elapsed_time (torchaudio; GPU): 0.002648[sec]
elapsed_time (torchlibrosa; CPU): 0.237388[sec]
elapsed_time (torchlibrosa; GPU): 0.209118[sec]

その2(5月8日版)

koukyo1213様のコメントに従って、torch.nn.Sequentialやtorchaudio.transforms.MelSpectrogramのインスタンス化部分を計測対象から外した場合の結果を以下に示す。

elapsed_time (librosa): 0.040965[sec]
elapsed_time (torchaudio; CPU): 0.014024[sec]
elapsed_time (torchaudio; GPU): 0.001927[sec]
elapsed_time (torchlibrosa; CPU): 0.027673[sec]
elapsed_time (torchlibrosa; GPU): 0.006538[sec]

今回もGPUを使ったtorchaudioが一番早い結果となった。

考察とまとめ

最初のスクリプトでtorchlibrosaの実行時間がより多くかかったのは、koukyo1213様ご指摘の通り、インスタンス化のオーバーヘッドがかなり大きいことに起因すると考えられる。その影響を除いた新たなスクリプトでは、GPU使用のtorchlibrosaは、同じくGPU使用のtorchaudioと比較して3倍ほど低速という結果が得られた。とはいえ実行時間の桁は揃い、どちらも十分実用に供すると考える。

black用py-isortの設定

私はblackをpythonコードのformatterに用いているので、blackの仕様に合うようにisortを設定して動かしたいというわけである。isortをEmacsから触るためにはpy-isortを用いる。以下の記事は参考になる。

qiita.com

isortのマニュアルはこちらから: pycqa.github.io

まずisortをインストールする。

pip3 install isort

py-isortはEmacsのパッケージシステムから容易にインストール可能である。その後、Emacs設定ファイルに以下を追記する。こうするとバッファ保存時にisortが走ってくれるので便利である(=つまりインポート順を自分で編集する必要がなくなる!)。

;; py-sort: isortを使ってライブラリのimport順をソート
(eval-when-compile (require 'py-isort))
;; for black
(setq py-isort-options '("--multi-line=3"
                         "--line-length=88"
                         "--trailing-comma"
                         "--use-parentheses"
                         "--ensure-newline-before-comments"
                         "--force-grid-wrap=0"))
;; バッファ保存時に自動ソート
(add-hook 'before-save-hook 'py-isort-before-save)

なおこれらのオプション値はblack公式のドキュメントを参考にした。 black.readthedocs.io