kivantium活動日記

プログラムを使っていろいろやります

銀髪美少女botを作った

この記事は創作+機械学習 Advent Calendar 2021の1日目です。

はじめに

Deep Learningが画像認識コンテストで優勝して以降、機械学習をはじめとする人工知能技術の研究開発は一大ムーブメントとなり、第3次AIブームと呼ばれる状況にあります。このブログでは機械学習を二次元画像に応用した記事を何回か書いてきましたが、機械学習は画像だけではなく音声や言語など様々な分野で応用されています。このAdvent Calendarでは皆様に機械学習の創作への応用事例を紹介していただきます。どんな記事が投稿されるのか楽しみにしています。また、このAdvent Calendarを読んだ方の中に自分でも機械学習をやってみようと思ってくださる方が一人でもいらっしゃれば大変嬉しいです。

12/1時点では10名の方に参加表明をいただいています。参加者の皆様にはこの場を借りてお礼申し上げます。まだまだ枠に余裕がありますので、記事を読んで自分も参加したいと思った方は是非飛び入り参加してみてください。 adventar.org

二次元イラスト収集サイト にじさーち

この記事では現在開発中の二次元イラスト収集サイトにじさーちと、そのデータベースを利用して作った銀髪美少女botを紹介します。にじさーちの開発中のバージョンは記事にしていたのですが、現在のバージョンは記事にしていなかったのでここで紹介しようと思います。

nijisearch.kivantium.net

画像の収集

にじさーちはTwitterに投稿された二次元美少女イラストを収集の対象としています。Twitterに毎日投稿される数多くの画像の中から美少女イラストだけを収集するために機械学習を利用しています。収集対象かどうかの判定は2段階で行っています。まず最初にPyTorchでファインチューニングしたモデルをONNXで利用する - kivantium活動日記で紹介したSqueezenetを使ってイラスト or イラストではない の2クラス分類を行います。イラストであると判定された画像に対してさらにIllustration2Vecによってタグ付けを行い、girlを含むタグが付いた画像を美少女イラストと判定して収集対象にしています。全ての画像に対してIllustration2Vecを呼び出すと計算機の負荷が大きかったため、まず軽量なモデルでスクリーニングを行う運用になりました。

どのツイートに対してイラストを含むかの判定を行うかも問題になるのですが、今のところ絵師リストの監視や、フォロワーのRT・いいねなどを対象にしています。お気に入り数が多い画像ツイートをTweet検索を用いて取得する方法なども検討したのですが、イラストを含む画像のヒット率が低かったです。また、最近はシャドウバンによってイラストが検索結果に表示されないことが増えているため、イラストを描く人やイラストをよくRTする人のアカウントを直接監視することが今後より重要になっていくと考えています。なお、pixiv等で知らない絵師を発見した場合は手動で登録することもあります。

画像の表示

こうして収集したイラストはデータベースに登録され、Djangoを用いて構築されたサイトで表示されています。デザインにはBootstrapMasonryを利用しています。なお、Twitterに投稿されたツイートの表示は、Twitter開発者利用規約のI-Bで許可されており、 著作権法第四十七条の五で定められた軽微利用の範囲でサムネイル画像等の表示を行っています。

Twitter利用規約がなかなか厄介で、ツイートを表示するときにはDisplay Requirementsに従う必要があるほか、削除されたツイートの内容を表示してはいけないなどの細かい規定があります。これに従うためにツイートの表示にはTwitterが提供する埋め込み機能を利用し、サムネイル画像のキャッシュなどは持たないようにしているのですが、これによりサイトの動作がかなり重くなっているように感じています。読み込み速度の改善は大きな課題です。

f:id:kivantium:20211130130049p:plain:w600
画像詳細ページのスクリーンショット。Illustration2Vecによるタグの表示とツイート埋め込みによる画像表示を行っている https://nijisearch.kivantium.net/status/1461207355572383750/

また、現在はIllustration2Vecによるタグ表示が中心になっていますが、Illustration2Vecでタグ付けができるキャラクター数はかなり限られています。同一キャラクターを判定する機械学習モデルを現在開発中なので、これを用いてキャラクターの自動タグ付けを可能にしたいと思っています。

銀髪美少女bot

こうしてTwitterに投稿されたイラストを日々収集していたある日、銀髪美少女好きのフォロイーが「銀髪美少女を自動で収集したい」的な発言をしているのを見かけました。Illustration2Vecに silver hair タグが存在するので、銀髪美少女データベースが既に手元にある状態でした。こうしてにじさーちからスピンオフしたプロジェクトが銀髪美少女botです。

f:id:kivantium:20211130133901j:plain:w500
https://twitter.com/silverhair_bot

仕組み

にじさーちに登録された画像のうちsilver hairまたはwhite hairタグがついていて500回以上RTされているものを5分に1度ランダムに1つ選んでリツイートしています。銀髪と白髪は非常に似ていてほとんど区別がつかないため、このような運用にしました。また、新着画像がどちらかのタグを含んでいた場合は50いいね以上でリツイートするようにしています。こうすることで過去の名作と直近の作品がバランスよく閲覧できるだろうと考えています。なお、上記のルールを満たさなかった場合でも目視判定して手動でRTすることがあります。

f:id:kivantium:20211130135022p:plain:w400
silver hairタグでにじさーちを検索した例 https://nijisearch.kivantium.net/search/?&tag=silver%20hair

ちなみに、にじさーちはRSS機能を提供しているので特定のタグの新着画像をSlackチャンネルに投稿するなどの使い方もできます。タグ検索したときに出てくる「検索結果: XXXX件」の右側のRSSマークをクリックするとRSSが表示されます。(maidタグのRSSの例

今後の課題

まず、イラストかそうでないかの判定が甘いことが第一の課題です。よくある誤判定の例としては、コスプレ画像をイラストと判定してしまうミスがあります。コスプレ画像は人間が見ればひと目でイラストでない画像だと見抜くことができるのですが、イラストと似たような色合いになっているため機械学習はうまく区別することができないようです。誤判定した画像を学習データに加えて何度も再学習しているのですが、未だに数多くの誤判定が起こっています。また、そもそも何をイラストと定義するべきかの問題もあります。ソシャゲのスクリーンショットもよく誤ってリツイートしてしまうのですが、キャラクターが描写されているイラストであることには変わりありません。ソシャゲのスクリーンショットを負例に加えて学習すると全体として精度が落ちる懸念があるため、今のところ正例にも負例にも加えずに学習させています。ソシャゲ以外にも、アニメのスクリーンショットや漫画の購入報告・無断転載画像など、イラストではあるのだが収集したくない画像はたくさんあります。コミケサークルカットを収集するべきかどうかも微妙な問題でした。画像が投稿された文脈を理解して収集するかどうかを判断するのは機械学習には難しいので人手によるスクリーニングに頼っているのが現状です。

髪色の判定も微妙な問題です。同じ髪の色でも光の当たり方によっては別の色で塗られることがあります。銀髪や白髪は特に色が薄いので光の影響を受けやすいように感じています。髪色が微妙な場合は髪色のタグが何もつかないことが多いのですが、極端な場合は別の髪色タグがつくこともあります。

髪色判定に失敗する画像の例。設定上灰色の髪とされているキャラなのだが、blonde hairタグがついている (https://nijisearch.kivantium.net/status/1133960305183039488/)

また、水色の髪と銀髪の区別も難しいと感じています(誤判定されるイラストの例)。設定上の髪色が水色の場合でも、銀髪設定のキャラとほとんど同じ色で塗られることがあるのでキャラごとに設定上の色が何なのかを調べる必要があります。これは画像だけで判定するのが不可能なのでキャラ判定を導入しないと解決できないと考えています。

以上、機械学習を用いてイラストを収集する事例について紹介しました。明日は@amane_lyricさんの「そのキャラを好きになった理由を探る」です。お楽しみに。

pybind11とxtensorによるPythonとC++の連携

前回の記事でNumCppを試しましたが、全ての配列を2次元配列で扱う仕様がいまいちだったのでEigenを使ったほうが良さそうだという結論になりました。 kivantium.hateblo.jp

この記事ではxtensorという別のライブラリを試してみます。xtensorの開発はQuantStackという会社が中心になって行われているようです。Webサイトを見る限りJupyterLabなどのOSSを開発している会社のようですが、どうやって儲けているのかいまいち分かりませんでした……

インストール

READMEを見るとcondaでのインストールが推奨されているのですが、header-onlyライブラリなのでソースコードを落とすだけで使うことが出来ます。Pythonバインディングを使うためには3つのプロジェクトが必要になります。ここでは作業ディレクトリで以下のコマンドを実行した場合を説明します。

git clone https://github.com/xtensor-stack/xtensor.git
git clone https://github.com/xtensor-stack/xtl.git
git clone https://github.com/xtensor-stack/xtensor-python.git

簡単な例

前回と同様に足し算の例で試してみます。以下のC++コードを example.cpp という名前で保存します。

#include <pybind11/pybind11.h>
#include <xtensor/xmath.hpp>
#define FORCE_IMPORT_ARRAY
#include <xtensor-python/pyarray.hpp>

xt::pyarray<double> add_arrays(xt::pyarray<double>& input1, xt::pyarray<double>& input2) {
    return input1 + input2;
}

PYBIND11_MODULE(example, m)
{
    xt::import_numpy();
    m.def("add_arrays", &add_arrays);
}

コンパイルコマンドは以下の通りです。長いですが、pybind11のコンパイルコマンドをC++14向けに変更してxtensorのディレクトリを追加しただけです。

c++ -O3 -Wall -shared -std=c++14 -fPIC $(python3 -m pybind11 --includes) -I xtensor/include/ -I xtl/include/ -I xtensor-python/include/ -DNUMCPP_INCLUDE_PYBIND_PYTHON_INTERFACE example.cpp -o example$(python3-config --extension-suffix)

コンパイルしたC++コードを実行するPythonコードは以下の通りです。

import numpy as np
import example

a = np.asarray([1, 2, 3, 4]) 
b = np.asarray([2, 3, 4, 5])

print(example.add_arrays(a, b)) # [3. 5. 7. 9.]

期待通りの1次元配列 (numpy.ndarray) が返ってきています(戻り値のコピーが行われているかどうかは調べてないです)。

2次元・3次元の場合も期待通りの結果になります。

import numpy as np
import example

a = np.asarray([[1, 2], [3, 4]])
b = np.asarray([[2, 3], [4, 5]])
print(example.add_arrays(a, b))
# [[3. 5.]
#  [7. 9.]]

a = np.asarray([[[1], [2]], [[3], [4]]]) 
b = np.asarray([[[2], [3]], [[4], [5]]]) 
print(example.add_arrays(a, b))
# [[[3.]
#   [5.]]
# 
#  [[7.]
#   [9.]]]

EigenのときはVectorとMatrixを区別する必要がありましたが、xtensorは次元数に関わらず同じ関数が使えるのでよりNumPyに近い使用感を実現できています。 他の関数の使用感は確認していませんが、チートシートを見る限りではかなりNumPyを再現できているように見えます。また、BLASバインディングを提供するxtensor-blasというプロジェクトもあるようなので、速度を求める人の需要もある程度は満たせそうな気がします。もう少し試してみてもいいかもしれません。

pybind11とNumCppによるPythonとC++の連携

Pythonで書いたコードの速度が遅いときに一部の関数だけC++で記述したい場合があります。pybind11を使えばC++Pythonを比較的容易に連携させることができるのですが、NumPyオブジェクトをC++に渡したときの処理の記述が非常に面倒です。そこでNumPyのC++版として開発されているライブラリのNumCppを使って、NumPyオブジェクトの受け渡しを行う方法を検討しました。

TL; DR 現時点ではNumCppよりもEigenを使うほうが良さそうです

pybind11だけを使った場合

まずはpybind11だけでNumPyを使うとどうなるかについて説明します。pybind11ドキュメントのNumPyのページにある足し算を行う例を紹介します。

py::array_t<double> add_arrays(py::array_t<double> input1, py::array_t<double> input2) {
    py::buffer_info buf1 = input1.request(), buf2 = input2.request();

    /* shapeのチェックは省略 */

    auto result = py::array_t<double>(buf1.size);

    py::buffer_info buf3 = result.request();

    double *ptr1 = static_cast<double *>(buf1.ptr);
    double *ptr2 = static_cast<double *>(buf2.ptr);
    double *ptr3 = static_cast<double *>(buf3.ptr);

    for (size_t idx = 0; idx < buf1.shape[0]; idx++)
        ptr3[idx] = ptr1[idx] + ptr2[idx];

    return result;
}

行列の要素にアクセスするためにわざわざポインタを経由する必要がある上に、単なる行列の足し算をわざわざループで書き直す必要があります。この例は1次元配列でしたが、2次元以上になるとさらに処理が面倒になります。そこでC++の行列演算ライブラリを組み合わせて簡単に記述できるようにします。

pybind11は公式でEigenとの連携をサポートしているのですが、Eigenを使う方法は既に日本語解説が存在しているのでここではNumCppを使ってみます。

NumCppを使った場合

インストール

pybind11のインストールはpipを使う方法が一番簡単です。その他の方法はドキュメントのInstalling the libraryを参照してください。

pip install pybind11

NumCppはheader-onlyライブラリなのでファイルをダウンロードするだけです。ここでは作業ディレクトリにcloneすることにします。

git clone https://github.com/dpilger26/NumCpp.git

簡単な例

NumCppを使って二つの行列の和を求める関数を書いてみます。

以下のC++ファイルを example.cpp という名前で保存します。

#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>

#include <NumCpp.hpp>

namespace py=pybind11;

py::array_t<double> add_arrays(py::array_t<double, py::array::c_style> input1, py::array_t<double, py::array::c_style> input2) {
    auto array1 = nc::pybindInterface::pybind2nc(input1);
    auto array2 = nc::pybindInterface::pybind2nc(input2);
    auto sum = array1 + array2;
    return nc::pybindInterface::nc2pybind(sum);
}

PYBIND11_MODULE(example, m) {
    m.def("add_arrays", &add_arrays);
}

Linuxの場合コンパイルは以下のように行います。

c++ -O3 -Wall -shared -std=c++14 -fPIC $(python3 -m pybind11 --includes) -I NumCpp/include/ -DNUMCPP_INCLUDE_PYBIND_PYTHON_INTERFACE example.cpp -o example$(python3-config --extension-suffix)

これはpybind11のコンパイルコマンドに NumCppのヘッダファイルの場所指定 (-I NumCpp/include/) と NumCppのpybind11オプション (-DNUMCPP_INCLUDE_PYBIND_PYTHON_INTERFACE) を追加してC++14を指定 (-std=c++14) したものです。CMakeを使う方法などもありますが、複雑なのでここでは解説しません。

コンパイルに成功すると example.cpython-***.so のようなファイルが生成されます。これを読み込むPythonプログラムは以下のようになります。

import numpy as np
import example

a = np.asarray([1, 2, 3, 4]) 
b = np.asarray([2, 3, 4, 5])
print(example.add_arrays(a, b).shape) # [[3. 5. 7. 9.]]  

正しく実行できているように見えるのですが、1次元配列同士の和なのに2次元配列が返ってきています。ドキュメントによると、NumCppのNdArrayクラスは2次元配列として実装されているため、1次元配列は1xNの2次元配列として扱う仕様になっているそうです。

2次元配列は問題なく処理できます。

import numpy as np
import example

a = np.asarray([[1, 2], [3, 4]]) 
b = np.asarray([[2, 3], [4, 5]])
print(example.add_arrays(a, b)) 
# [[3. 5.]
#  [7. 9.]]

しかし、3次元配列を渡すとエラーが起こります。

import numpy as np
import example

a = np.asarray([[[1], [2]], [[3], [4]]]) 
b = np.asarray([[[2], [3]], [[4], [5]]]) 
print(example.add_arrays(a, b)) 

エラーメッセージ

File: NumCpp/include/NumCpp/PythonInterface/PybindInterface.hpp
    Function: pybind2nc
    Line: 88
    Error: input array must be no more than 2 dimensional.Traceback (most recent call last):
  File "test.py", line 6, in <module>
    print(example.add_arrays(a, b)) 
ValueError: File: NumCpp/include/NumCpp/PythonInterface/PybindInterface.hpp
    Function: pybind2nc
    Line: 88
    Error: input array must be no more than 2 dimensional.

多次元配列についてのissueを見る限り3次元以上の配列はまだ実装されてなさそうなので、現時点でNumPy互換と呼ぶのはちょっと難しいかもしれません。

おまけ: Eigenを使った場合

参考としてEigenを使った場合を示しておきます。MatrixをRowMajorにすることと、VectorとMatrixを区別することだけ気をつければそんなに難しくないので、よりメジャーなEigenを使うほうが良い場合が多いと思います。

#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>

#include <Eigen/Dense>

namespace py = pybind11;

using RowMatrixXd = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;

template <typename T>
T add_arrays(Eigen::Ref<const T> input1, Eigen::Ref<const T> input2) {
    return input1 + input2;
}

PYBIND11_MODULE(example, m)
{
    m.def("add_vectors", &add_arrays<Eigen::VectorXd>);
    m.def("add_matrices", &add_arrays<RowMatrixXd>);
}

Pythonはこんな感じです。

import numpy as np
import example

a = np.asarray([1, 2, 3, 4]) 
b = np.asarray([2, 3, 4, 5])
print(example.add_vectors(a, b))  # array([3., 5., 7., 9.])

a = np.asarray([[1, 2], [3, 4]])
b = np.asarray([[2, 3], [4, 5]])
print(example.add_matrices(a, b))
# array([[3., 5.],
#        [7., 9.]])

参考文献

1つ目の記事のコメントによるとxtensorというライブラリがあり、これもpybind11連携ができるようなのですが、インストールにcondaが必要そうなのでまだ試していません。誰か試していただけると嬉しいです。

追記 xtensorを試しました。condaは必要ありませんでした。 kivantium.hateblo.jp

創作+機械学習 Advent Calendar 2021 を開催します

このAdvent Calendarは創作(小説、漫画、アニメ、イラスト、映画、音楽、ゲーム等)と機械学習に関連した記事を投稿してもらい、優れた記事を書いた方に賞を贈呈する企画です。

2019年に@thetenthartさん主催の創作+機械学習LT会というイベントがあったのですが、コロナの影響で第2回が開催できていませんでした。そこで@thetenthartさんと@xbar_usuiさんの協力を得て、Advent Calendarという形で後継のイベントを企画させていただきました。皆様のご参加をお待ちしております。

adventar.org

f:id:kivantium:20211104124330p:plain:w600
告知画像(@yumu_7さんに作っていただきました)

ルール

  • 参加の意思表示はAdventarに記事公開日を登録することで行います。
  • 登録日になったら創作(小説、漫画、アニメ、イラスト、映画、音楽、ゲーム等)と機械学習に関連する記事を公開してください(追記: 「記事」はインターネット上で自由に閲覧できるものであれば形式は問いません。ブログ・動画・漫画などを想定しています。機械学習で生成した創作物のみでも良いですが、機械学習がどう使われているかが分かる解説があることが望ましいです。)
  • 2021年内に記事を公開された方を賞の審査対象とします(追記: 空き枠に後から登録することも可能です)
  • 2022年1月に参加者の相互投票で優秀賞を決定します。
  • 賞に関する連絡を送る場合がありますので、Twitter@kivantiumからのDMを受信できる状態にするか、メールアドレスなどの連絡先を記事中に記載してください。
  • 記事を投稿してくださった方にはコミケや技術書典等で出版する同人誌への投稿を後日依頼する可能性があります。

記事の書き方

  • 記事の冒頭に創作+機械学習 Advent Calendar 2021 の記事であることを明記してください。
  • 記事の内容は創作と機械学習に関連していればなんでもありです。未完成のものや企画案などでも大丈夫です。
  • 機械学習の定義はなるべく広く取ります。あなたが機械学習だと思うものが機械学習です。
  • 記事は日本語または英語で書いてください。
  • 一人で複数の記事を投稿しても問題ありません。カレンダーの枠が埋まった場合は2枚目を作成します。

テーマの例

  • 画像関係: 画像分類・画像生成・自動着色・超解像など
  • 言語関係: 小説生成・チャットボットなど
  • 音声関係: 音楽生成・声質変換など
  • 創作支援: 機械学習による創作支援ツールの紹介・ツールに支援されて作った作品の紹介など
  • その他: ゲームの自動プレイ・推薦システムの構築など

上に挙げた以外のテーマでももちろん大丈夫です。未完成のものやアイデア段階の記事も歓迎します。「〜というデータを集めて、自動で〜する機械学習モデルを作りたいと思っています。協力者募集」みたいな感じでも問題ありません。2019年のLT会のスライドも参考になると思います。

参考までに本ブログで関係しそうな過去記事を紹介しておきます。

賞について

以下の賞を用意しています。審査員特別賞を提供してくださる方がいらっしゃったら連絡してください。

  • 最優秀賞 (1名) 賞金20000円
  • 優秀賞(2〜3名)賞金10000円
  • 審査員特別賞
    • @kivantium賞(機械学習の使い方に新しさを感じる記事を書いた方に授与)
    • @thetenthart賞(創作支援に役立ちそうな記事を書いた方に授与)
    • @xbar_usui賞(機械学習を知らない人でも楽しめる記事を書いた方に授与)

賞金はAmazonギフトカードでの支払いを予定していますが、希望があれば銀行振込やビットコイン支払いなどにも柔軟に対応します。 皆様の投稿をお待ちしております。

【宣伝】創作と機械学習に関するDiscordサーバーがあるので、興味がある方はご参加ください。本Advent Calendarの運営についてもこのDiscordで議論しています

招待リンク: https://discord.gg/jQNXjkrqGU

FlaskでWebSocketを使う

WebSocketを使うとリアルタイム性の高いWebアプリケーションを作ることができます。今回はFlaskでWebSocketを使うライブラリのFlask-SocketIOを使って、複数のブラウザでテキストボックスを同期するアプリを作成します。

インストール

Flask-SocketIOはpipでインストールできます。

pip install flask-socketio

サンプル

Google Documentの同時編集みたいなイメージで、以下の機能を持つアプリを作成します。

  • 接続中のユーザー数が表示される
  • 接続中のユーザー全員でテキストボックスに入力した内容が同期される

DOMの操作はjQueryで行うことにします。作成したソースコードは以下の通りです。

server.py

from flask import Flask, render_template, request
from flask_socketio import SocketIO, send, emit

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'

# cors_allowed_originは本来適切に設定するべき
socketio = SocketIO(app, cors_allowed_origins='*')

# ユーザー数
user_count = 0
# 現在のテキスト
text = ""

@app.route('/')
def index():
    return render_template('index.html')

# ユーザーが新しく接続すると実行
@socketio.on('connect')
def connect(auth):
    global user_count, text
    user_count += 1
    # 接続者数の更新(全員向け)
    emit('count_update', {'user_count': user_count}, broadcast=True)
    # テキストエリアの更新
    emit('text_update', {'text': text})


# ユーザーの接続が切断すると実行
@socketio.on('disconnect')
def disconnect():
    global user_count
    user_count -= 1
    # 接続者数の更新(全員向け)
    emit('count_update', {'user_count': user_count}, broadcast=True)


# テキストエリアが変更されたときに実行
@socketio.on('text_update_request')
def text_update_request(json):
    global text
    text = json["text"]
    # 変更をリクエストした人以外に向けて送信する
    # 全員向けに送信すると入力の途中でテキストエリアが変更されて日本語入力がうまくできない
    emit('text_update', {'text': text}, broadcast=True, include_self=False)


if __name__ == '__main__':
    # 本番環境ではeventletやgeventを使うらしいが簡単のためデフォルトの開発用サーバーを使う
    socketio.run(app, debug=True)

templates/index.html

<html>
  <head>
    <title>FlaskでWebSocket</title>
  </head>
  <body>
    <p>現在の接続者数<span id="user_count"></span></p>
    <textarea id="text" name="text" rows="10" cols="60"></textarea>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>

    <script type="text/javascript" charset="utf-8">
      var socket = io();

      // 接続者数の更新
      socket.on('count_update', function(msg) {
        $('#user_count').html(msg.user_count);
      });
      
      // テキストエリアの更新
      socket.on('text_update', function(msg) {
        $('#text').val(msg.text);
      });
      
      // テキストエリアが変更されると呼び出される
      $('#text').on('change keyup input', function() {
        socket.emit('text_update_request', {text: $(this).val()});
      });
    </script>
  </body>
</html>

グローバル変数で接続者数をカウントするのはスレッドセーフではないのですが、GitHubでの作者コメントを読む限りでは通常このコードで問題ないようです。

python server.pyで実行して、ブラウザの画面を複数開くと同期している様子を見ることができます。 入力や削除の様子まで同期されていることが分かると思います。

f:id:kivantium:20211018102437g:plain

以上です。

Twitterのハッシュタグを正しく抽出する

ここ一年くらいかけて暇な時間にTwitterに投稿されたイラストを収集するにじさーちというサイトを開発しています。
にじさーちにはハッシュタグを使って画像を検索する機能があるのですが、ハッシュタグに中黒(・)が含まれる場合にうまく抽出できないバグがありました。

例えば次のツイートには「リネット・ビショップ生誕祭2020」というハッシュタグが含まれています。しかし、今まで使っていた正規表現では中黒でハッシュタグが終わると判定されてしまい「リネット」というタグがついていると認識されていました。

ハッシュタグ正規表現で抽出するのはかなり難しい

Twitterでは#で始まる文字列がハッシュタグとして利用されていますが、多言語対応を考えるとハッシュタグがどこで終わるのかを正確に判定する正規表現を書くのはかなり難しいです。例えば、hashtag regex in python · GitHubでは

hashtag_re = re.compile("(?:^|\s)[##]{1}(\w+)", re.UNICODE)

のようにハッシュタグを抽出しています。にじさーちでは昨日までこの正規表現をコピペして使っていたのですが、中黒(・)がハッシュタグの一部として認識されていませんでした(おそらく「・」がUnicode word charactersに含まれず\wにマッチしなかったのでしょう)

また、Twitterのソースコードから生成した正規表現だと主張するStack Oveflowのコードも中黒(・)をうまく認識できませんでした。

ハッシュタグ正規表現の歴史

Twitterの中の人ではないので詳しい歴史的経緯は分かりませんが、日本語ハッシュタグ導入時のtogetterを読むと、導入当時は中黒(・)がハッシュタグ区切りとして導入されているようでした。


(現在は・がハッシュタグの一部として認識されているのでこれだけ見てもよく分かりませんが、togetter上では次の写真のように表示されているので、当時は・がハッシュタグの区切りになっていたことが推測できます)
f:id:kivantium:20210829154405p:plain

しかし、2017年12月16日のInitial commit of twitter-text 2.0 · twitter/twitter-text@34dc1dd · GitHubでは中黒に対応するU+30FBがhashtagSpecialCharsとして追加されています。そのため、2011年から2017年のどこかのタイミングで中黒の処理に変更が入ったことが推測できます。2015年のStack Overflowの回答にある正規表現が現在の仕様と合致しないのはTwitter側の仕様変更が理由である可能性が高いです。

解決策その1: 公式ライブラリtwitter-textを使う

多言語に対応するハッシュタグ正規表現を自分で実装するのは非常に手間がかかりそうです。しかし、Twitter社が公式でテキストをパースするためのライブラリを提供しているので対応言語の場合はライブラリを使うことが出来ます。
github.com

JavaScriptの場合はこのように使うことができます。

var parser = require('twitter-text')
var tweet = "#リネット・ビショップ生誕祭2020\n間に合った!!リーネちゃん誕生日おめでとう!";
var result = parser.extractHashtags(tweet);
console.log(result);  // [ 'リネット・ビショップ生誕祭2020' ]

しかし、現在ライブラリが正式に提供されているのはJava, Ruby, JavaScript, Objective-Cのみで、Pythonは含まれていません。移植すればいいのかもしれませんが、Unicode周りを正しく書ける知識は私にはありません。(Python移植であると主張するレポジトリはあるのですが、日本語ではうまく動きませんでした)

解決策その2: Twitter APIに含まれるハッシュタグ情報を使う

Pythonによる文字列処理でハッシュタグを抽出するのは難しそうなのですが、Twitter APIを使ってツイートを読み込む際にハッシュタグ情報を取得することができます。

Twitter APIGET statuses/show/:idのドキュメントを読むと、Example Responseの"entities"の中に"hashtags"という項目があります。ここにハッシュタグの情報が含まれています。

Tweepyを使う場合は次のようにハッシュタグを取得できます。

status = api.get_status(1271018594273333248)  # 冒頭のツイート
for tag in status.entities['hashtags']:
    print(tag['text'])  # リネット・ビショップ生誕祭2020

Twitter APIを使うことで、文字列処理に頼らずにTwitterが認識する通りのハッシュタグを取得することができました。

(今回例に使ったツイートは最初にバグに気がついた例として記録しておいたものです。バグに気づいてから1年以上放置していたようです……)

OpenAI Gym Roboticsの環境構築とBaselinesの実行

OpenAIが提供している強化学習環境のGymを使って、OpenAI BaselinesのHER (Hindsight Experience Replay) を実行するまでの手順について記録しておきます。環境構築はUbuntu 20.04で行いました。

MuJoCoのライセンス取得

Gymで提供されている環境のうち、3Dを使うものはMuJoCoに依存しています。今回使いたいRobotics環境は3Dなのでライセンスを取得する必要があります。

ライセンスの取得はLicenseから行います。30日間の試用期間中は無料で利用できますが、その後は正式なライセンスを取得する必要があります。学生の個人利用は無料ですが、学生でない場合は個人の非商用利用で年間$500、商用利用では年間$2000掛かります。ぼったくり

私はまだ学生なので、学生個人利用ライセンスを申請しました。申請から数日後にaccount numberがメールで送られてきました。ライセンスページで配布されているgetidなるバイナリを実行して得られるComputer idとaccount numberを登録するとmjkey.txtというファイルをダウンロードできます。これを~/.mujoco/に保存します。

Productsで配布されているmujoco200 linuxをダウンロードします。これを解凍して~/.mujoco/mujoco200/に置きます。

~/.bashrc に以下を追記して(usernameは自分の名前にする)、シェルを再起動します。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/username/.mujoco/mujoco200/bin

必要ライブラリのインストール

OpenAI Gym Roboticsの実行に必要なライブラリ

GymのREADMEに従って以下をインストールします。

sudo apt-get install -y libglu1-mesa-dev libgl1-mesa-dev libosmesa6-dev xvfb ffmpeg curl patchelf libglfw3 libglfw3-dev cmake zlib1g zlib1g-dev swig

MuJoCoに依存する環境を実行するためにはmujoco-pyが必要になります。

pip install -U 'mujoco-py<2.1,>=2.0'

Baselinesの実行に必要なライブラリ

BaselinesのREADMEに従って

sudo apt-get update && sudo apt-get install cmake libopenmpi-dev python3-dev zlib1g-dev

を実行します。

次にTensorFlowをインストールする必要があるのですが、今のところTensorFlow 1.14が推奨されています。自分が使っているPython環境では古すぎてインストールできなかったため、Tensorflowの旧バージョンをインストールする - Qiitaに従ってpyenvでPython 3.7.2の環境を構築してそこでインストールを行いました。

pip install tensorflow==1.14.0

その他、ドキュメントに書いていない必要なライブラリをインストールするために以下を実行します。

pip install matplotlib pandas atari_py filelock mpi4py pytest

Baselinesのインストール

OpenAI Baselinesをインストールします。

git clone https://github.com/openai/baselines.git
cd baselines
pip install -e .

インストールが終わったらテストを実行します。テストが通らなかったら適宜対応してください。

pytest

OpenAI Gym Roboticsの動作確認

まずはOpenAI Gym Roboticsが正しく動くか確認します。以下のコードを実行します。

import gym

env = gym.make('FetchReach-v1')
while True:
    observation = env.reset()
    while True:
        env.render()
        action = env.action_space.sample()
        observation, reward, done, info = env.step(action)
        if done:
            print("Done!")
            break

ランダムに選んだ動作を無限に続けるコードになっています。正しくインストールされていれば、以下のような画面が表示されます。 f:id:kivantium:20210208215102p:plain:w600

Baselinesの実行

Baselinesに実装されているHER (Hindsight Experience Replay) を実行します。このアルゴリズムは報酬がスパースな環境(例えばロボットアームによる操作が成功したかしなかったかのどちらかしか分からないような環境)でうまく強化学習を行うために開発されたアルゴリズムです。現在の方策がゴールに到達できなかった場合でも、現在の方策で実行できた範囲のものをゴールと設定して報酬を与えることで報酬のスパースさを解決しているようですが、詳細は今度実装したときにでも書きます。

OpenAIによるHERのベースライン実装を実行するには、先にインストールしたbaselinesディレクトリで

python -m baselines.run --alg=her --env=FetchReach-v1 --num_timesteps=5000 --play

を実行すればよいです。ここで実行するFetchReach-v1は、ランダムに選ばれた3次元空間上の点に手を動かすだけの簡単なタスクなので、CPUのみの環境でもすぐに100%成功するようになります。学習結果はこんな感じになりました。

次回があれば、PyTorchあたりで強化学習アルゴリズムを何か実装します。

参考サイト

広告コーナー