雑記: fastText のチュートリアルをやるだけ

インストール

以下の記事にしたがうと Ubuntu on Windows Subsystem for LinuxfastText をインストールできます。

以下のコマンドで usage が出てくればインストールできていると思います。

./fasttext

出てくる usage をみると、fasttext コマンドには以下のモードがあるようです(空欄は書き途中)。公式のチートシートCheatsheet · fastText )も参照してください。

supervisedラベル付きの教師データから分類器を学習する(オプション: たくさんあって例えば以下)。
  • -dim: 単語ベクトルの次元数(デフォルト 100)。
  • -lr: 学習率(デフォルト 0.1)。
  • -epoch: エポック数(デフォルト 5)。
上のようなハイパーパラメータはオートチューニングさせることもできる。オートチューニングに関するオプションは以下。
  • -autotune-validation: これに評価用データを渡すとオートチューニングする。
  • -autotune-duration: オートチューニングの探索時間(秒)。
  • -autotune-modelsize: モデルサイズを制限するとき指定する(Ex. 2M)。
quantizeラベル付きの教師データから分類器を学習する(オプション: たくさん)。メモリ使用量を減らすためモデルを量子化するバージョン。
test学習した分類器と任意のデータに対して精度と感度を出す(オプション: 予測ラベル数、予測確率の閾値)。
test-label学習した分類器と任意のデータに対してラベルごとに精度と感度を出す(オプション: 予測ラベル数、予測確率の閾値)。
predict学習した分類器で予測する(オプション: 予測ラベル数、予測確率の閾値)。
predict-prob学習した分類器で予測する(予測ラベルだけでなく確率値も出力する)(オプション: 予測ラベル数、予測確率の閾値)。
skipgramskipgram アルゴリズムで単語ベクトルを学習する(オプション: たくさん)。
cbowcbow アルゴリズムで単語ベクトルを学習する(オプション: たくさん)。
print-word-vectors学習した分類器と任意のデータに対してデータに含まれる単語のベクトルを出力する。
print-sentence-vectors学習した分類器と任意のデータに対してデータに含まれる文章の平均ベクトルを出力する。
print-ngrams
nn学習した分類器に対して指定した単語の nearest neighbors を表示する(オプション: 上位何件表示するか)。
analogies学習した分類器に対して指定した 単語A - 単語B + 単語C の nearest neighbors を表示する(オプション: 上位何件表示するか)。
dump学習した分類器から情報をダンプする(オプション: args, dict, input, output)。

複数のラベルが付いた文章を分類する

公式のチュートリアルをやります。

データの取得
上のページの Getting and preparing the data に倣ってまずデータをダウンロードします。

wget https://dl.fbaipublicfiles.com/fasttext/data/cooking.stackexchange.tar.gz && tar xvzf cooking.stackexchange.tar.gz

readme.txt を読むとこれは以下のサイトのデータとのことです。料理に関する質問サイトのようです。個々の質問にはいくつかのタグがついています(このタグがデータのラベルに対応しています)。

cooking.stackexchange.txt をみると、15404 行あって、以下のように各行が「ラベル1 ラベル2 文章」のようになっています(ラベルの数は不定)。複数のラベルが付いた学習データのようです。__label__ というのは fasttext がラベルと認識してくれるプレフィックスです(supervised モードの -label オプションで異なるプレフィックスも指定できます)。このようなデータを自分で用意すれば自分のデータも学習できます(日本語の場合は単語ごとにわかち書きしておく必要がありますが)。

__label__sauce __label__cheese How much does potato starch affect a cheese sauce recipe?
__label__food-safety __label__acidity Dangerous pathogens capable of growing in acidic environments
__label__cast-iron __label__stove How do I cover up the white spots on my cast iron stove?

チュートリアルでは最初の 12404 行を訓練用データ、最後の 3000 行を評価用データに切り分けているのでそれに倣います。

head -n 12404 cooking.stackexchange.txt > cooking.train
tail -n 3000 cooking.stackexchange.txt > cooking.valid

分類器の学習
文章のラベルを予測する分類器を学習します。supervised モードで以下のように実行します。

fasttext=/mnt/c/linux_home/fastText/fasttext
$fasttext supervised -input data/cooking.train -output model_cooking

以下のように学習が進み、model_cooking.bin と model_cooking.vec が生成されます。

Read 0M words
Number of words:  14543
Number of labels: 735
Progress: 100.0% words/sec/thread:   10556 lr:  0.000000 avg.loss: 10.200933 ETA:   0h 0m 0s

生成された model_cooking.vec の中身をみてみます。1行目は「14543 単語を 100 次元ベクトルに埋め込んだ」という意味です。実際、3行目は to という単語のベクトル表現、4行目は a という単語のベクトル表現です。となると、2行目は </s> という単語のベクトル表現にみえますが、</s> は実際の単語ではないです。これについては後述します。

14543 100
</s> -0.19674 0.79774 -0.32248 -0.48751 0.81991 0.44264 0.023684 -0.13507 0.93978 0.9933 -0.51045 0.1542 0.30656 0.7901 -0.37725 -0.11613 0.068653 0.16041 0.0173 -0.55148 -0.026323 0.75483 0.50935 -0.52323 0.0079467 -0.61519 0.48465 -0.63781 -0.40468 0.98114 -0.36877 -0.16163 -0.74467 -0.72118 -1.0947 0.037487 0.105 -0.61662 -0.16637 -0.3349 0.34562 0.90722 0.26701 0.58475 0.27875 -0.080856 0.067838 -0.22352 -0.48386 0.45114 -0.95859 0.62402 -0.82272 0.63454 -0.15089 -0.32259 -0.031502 0.26261 0.65062 0.36968 -0.4895 0.5752 1.0421 0.72295 -0.30713 -0.01582 -0.7944 0.30934 0.16335 0.42403 -0.88169 0.29987 -0.094941 -0.052784 -0.13877 -0.0081362 0.9565 0.76743 0.34506 -0.36395 -0.29721 0.442 0.93896 -0.53675 0.10719 -0.44839 0.20822 0.49516 -0.47837 -0.2682 0.86738 0.34557 1.3461 -0.83188 -0.17911 -1.0894 0.054833 0.7003 -0.56919 -0.45242
to -0.067108 0.39705 -0.16521 -0.26775 0.43871 0.27861 -0.034769 -0.096002 0.41407 0.4458 -0.075386 0.090708 0.052289 0.30679 -0.1375 -0.02269 0.017723 0.056648 -0.090194 -0.23134 0.064546 0.33595 0.14719 -0.16548 -0.025145 -0.26569 0.17582 -0.21569 -0.19566 0.32286 -0.12209 -0.091952 -0.31513 -0.29806 -0.42302 -0.021129 -0.0080154 -0.27211 -0.085119 -0.14138 0.15414 0.40777 0.067376 0.23067 0.12346 -0.0075088 0.00037916 -0.1155 -0.22735 0.13175 -0.42807 0.242 -0.3053 0.22256 -0.10684 -0.19737 -0.083466 0.12622 0.33479 0.14673 -0.14189 0.19358 0.41584 0.24277 -0.056506 0.024699 -0.39026 0.11678 0.056295 0.1298 -0.32616 0.14656 0.020648 -0.066931 -0.074845 -0.088811 0.37498 0.31593 0.18227 -0.1315 -0.10712 0.17731 0.38733 -0.14194 0.0040193 -0.19983 0.052696 0.14076 -0.23756 -0.10201 0.35857 0.12664 0.536 -0.37403 -0.0038928 -0.44641 0.071153 0.24687 -0.25552 -0.14378
a 0.012492 0.17169 -0.06459 -0.16798 0.2584 0.047045 -0.077651 -0.12495 0.29964 0.30246 -0.2987 0.047268 0.065618 0.16571 -0.17125 -0.06577 0.053795 0.052671 -0.016843 -0.10435 -0.0035953 0.18633 0.121 -0.11621 0.030406 -0.19042 0.19844 -0.3123 -0.22215 0.39258 -0.16879 0.02168 -0.22376 -0.3151 -0.52275 -0.025202 -0.10059 -0.13855 0.037249 -0.020664 -0.041001 0.38875 0.15119 0.21119 0.13383 -0.074202 0.1663 -0.033014 -0.13108 0.3875 -0.3598 0.23587 -0.29794 0.27076 -0.062832 0.037349 -0.051845 0.04353 0.14301 0.14153 -0.078114 0.35326 0.47126 0.32361 -0.26027 -0.056849 -0.27632 0.081134 0.093328 0.0080092 -0.20814 0.14146 -0.092766 -0.016157 -0.022588 0.092956 0.37898 0.20713 0.14062 -0.10115 -0.035477 0.14569 0.29206 -0.27978 0.040004 -0.18043 0.12128 0.23777 -0.10417 -0.08892 0.28171 0.14071 0.58662 -0.28369 -0.12543 -0.3648 -0.037512 0.31876 -0.089494 -0.21092

model_cooking.bin の方はバイナリなので中身を直接読めませんが、dump モードで情報を表示させることができます。args という引数を渡すと以下のように学習の設定が表示できます。

$fasttext dump model_cooking.bin args
dim 100   # 単語ベクトルの次元数
ws 5   # ウィンドウサイズ(supervised ではこの値は使われないらしい ※)
epoch 5   # エポック数
minCount 1   # 登場回数がこの数未満の単語は無視
neg 5    # 負例のサンプリング数(ベクトル表現を学習するとき中心語と文脈語の対かどうかの識別器を学習するのでそれだと思う)
wordNgrams 1
loss softmax
model sup
bucket 0
minn 0
maxn 0
lrUpdateRate 100
t 0.0001

※ については以下のスライドを拝見しました。

args の代わりに dict と指定すると訓練させたデータに含まれていた単語やラベルの集計が表示されます。input と指定すると数値が大量に表示されますが、これは model_cooking.vec と同一で、モデルが学習したベクトル表現のようです。output と指定するとやはり数値が大量に表示されます。これは正確に調べていませんが、fastText は多項ロジスティック回帰によって分類しているらしいので、その係数なのではないかと思います。

分類器による予測
学習した model_cooking.bin で実際に予測するには predict モードと predict-prob モードを使います。predict モードだと予測ラベルのみ出力されますが、predict-prob モードではラベルの確率値も一緒に出力されます。

$fasttext predict model_cooking.bin data/cooking.train > predicted_label.train
$fasttext predict model_cooking.bin data/cooking.valid > predicted_label.valid

データの代わりに - を引数に渡すと、タイプした文章にインタラクティブにラベルを予測してくれます。また、予測ラベル数や予測確率の閾値を指定することもできます。例えば predict モードで予測ラベル数を 3 に指定すると以下です。

$fasttext predict model_cooking.bin - 3
How much does potato starch affect a cheese sauce recipe?
__label__baking __label__food-safety __label__bread

predict-prob モードだと以下です。確率値も出ます。

$fasttext predict-prob model_cooking.bin - 3
How much does potato starch affect a cheese sauce recipe?
__label__baking 0.023058 __label__food-safety 0.0222845 __label__bread 0.0182505

最大のラベルでも 0.023 というのは小さいですね(といってもラベルは 735 個ありますが)。あとこのデータの真の正解ラベルである sauce も cheese も上位3件に入っていないですね…。

分類器の評価
test モードで分類器を評価できます。訓練データに対する評価は以下です。

$fasttext test model_cooking.bin data/cooking.train
N       12404
P@1     0.136
R@1     0.0589

評価データに対する評価は以下です。指標の値は訓練データに対するそれと近い気がします。

$fasttext test model_cooking.bin data/cooking.valid
N       3000
P@1     0.142
R@1     0.0613

引数に数値を渡すと P@k、R@k が計算できます。k=3 にすると k=1 より精度が下がり、感度が上がります。

$fasttext test model_cooking.bin data/cooking.train 3
N       12404
P@3     0.0819
R@3     0.107
$fasttext test model_cooking.bin data/cooking.valid 3
N       3000
P@3     0.0829
R@3     0.108

というか P@k と R@k の正確な定義がよくわかりませんが、ソースをみればいいと思います。

P@k と R@k の k は推論の際の予測ラベル数です。 各データに不定数の正解ラベルとk個の予測ラベルが付いていることになります。meter.cc の Meter::log() という関数の gold が全データの正解ラベルの総数(なぜ gold という変数名なのかわかりません)、predicted が全データの予測ラベルの総数、predictedGold が全データの予測ラベルの中でそのデータの正解ラベルに含まれていたものの総数です。そして、 P@k と R@k は

  • P@k = predictedGold / predicted
  • R@k = predictedGold / gold

です。test-label モードで実行するとラベルごとに指標が出せます。表にまとめるとこうです。

ラベル 正解ラベルにそのラベルを含むデータ数 予測ラベル(Top-k)にそのラベルを含むデータ数 予測ラベル(Top-k)にも正解ラベルにもそのラベルを含むデータ数 精度(Precision) 感度(Recall)
label1 gold1 predicted1 predictedGold1 predictedGold1 / predicted1 predictedGold1 / gold1
label2 gold2 predicted2 predictedGold2 predictedGold2 / predicted2 predictedGold2 / gold2
すべて Σ goldi Σ predictedi Σ predictedGoldi Σ predictedGoldi/ Σ predictedi Σ predictedGoldi/ Σ goldi
実際、test-label モードで実行すると以下のようにラベルごとに Precision と Recall がプリントされます。これをみると、Top-1 の予測では全てのデータが __label__baking か __label__food-safety か __label__substitutions のいずれかにしか予測されていないという事実がわかります(それ以下のラベルが全て Precision が None、Recall がゼロなので)。

$fasttext test-label model_cooking.bin data/cooking.train
F1-Score : 0.242344  Precision : 0.155056  Recall : 0.554498   __label__baking
F1-Score : 0.190454  Precision : 0.107175  Recall : 0.854188   __label__food-safety
F1-Score : 0.340326  Precision : 0.388988  Recall : 0.302486   __label__substitutions
F1-Score : 0.000000  Precision : --------  Recall : 0.000000   __label__equipment
F1-Score : 0.000000  Precision : --------  Recall : 0.000000   __label__bread
$fasttext test-label model_cooking.bin data/cooking.valid
F1-Score : 0.233992  Precision : 0.151484  Recall : 0.513889   __label__baking
F1-Score : 0.205660  Precision : 0.116205  Recall : 0.893443   __label__food-safety
F1-Score : 0.344023  Precision : 0.401361  Recall : 0.301020   __label__substitutions
F1-Score : 0.000000  Precision : --------  Recall : 0.000000   __label__equipment
F1-Score : 0.000000  Precision : --------  Recall : 0.000000   __label__bread

その他の機能
print-word-vectors モードでは、学習した分類器を指定して標準入力から適当なデータ(単語、単語列、文章)を渡すと、そのデータに含まれる単語のベクトルを出力します。

$fasttext print-word-vectors model_cooking.bin < data/cooking.valid   # ファイルから
$fasttext print-word-vectors model_cooking.bin   # インタラクティブ
How much does potato starch affect a cheese sauce recipe?
How 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
much 0.22722 0.91002 -0.84094 0.30225 -0.30555 -0.20168 -0.21758 -0.6925 0.050714 1.0291 0.18715 -0.4157 0.46272 -0.70559 0.73065 -0.31759 -1.1065 0.33034 -0.32165 0.042642 0.14589 0.46961 0.45965 0.67638 -1.1002 -1.2409 0.36938 -0.90074 0.20934 1.4298 1.3984 -0.82588 0.10168 1.2704 -0.86179 0.96689 -1.0505 0.11038 -0.42924 -0.049286 -0.19397 0.98325 -0.56746 -1.0483 -0.30183 -0.35669 0.042016 0.27229 0.20964 0.60953 -0.3668 -1.015 0.37869 0.33717 0.71164 -0.42239 0.022877 1.1084 1.0838 -0.87779 0.12118 0.23196 -0.20347 -0.6039 -0.21821 0.49487 -0.10744 -0.14096 -0.30951 -0.095197 0.49528 -0.51653 -0.75525 0.42532 -0.17701 -0.38641 -0.57726 0.16482 0.4644 0.4653 0.26798 0.5425 -0.37393 -0.32621 0.51616 0.57581 -0.93669 0.33229 -0.069224 -1.0827 0.059751 -0.42559 0.10031 -0.25191 0.28528 0.13178 -0.18955 -0.54339 -0.14521 -0.80504
does 0.32834 -1.19 0.54817 -0.36809 -0.19435 1.6243 -0.91674 0.15332 -0.26709 -0.014865 -0.33856 0.27598 -1.0753 0.54718 -0.63769 0.87679 0.18369 0.30915 0.37365 -0.50254 -0.96701 0.019201 0.059845 0.94462 -0.88541 -0.35104 0.034312 -0.42082 1.489 1.3392 -0.87674 -0.16115 0.83928 0.072629 0.11015 0.38114 -0.038614 -0.45594 -0.19126 1.7607 0.76206 -0.74446 -0.12468 -0.3788 -0.55383 0.083704 0.12357 -0.4626 -0.3055 0.048537 0.76693 0.76688 0.19337 0.61716 -0.82591 0.71833 0.57165 -0.76735 0.17664 -0.73828 -1.431 -0.042516 -0.16645 0.81091 -0.1387 -0.50888 0.59268 0.13015 -0.18835 0.65158 0.019614 -0.20911 -0.061807 0.58231 -0.45009 0.52177 -0.056014 -0.29689 0.37619 0.12884 -0.84277 -0.10887 0.36958 0.18755 -0.20866 -0.16468 -0.6531 0.21326 -0.039295 0.55748 0.87626 0.70385 0.32218 -0.25354 -0.11062 -0.43616 -0.71879 -0.96103 0.66443 0.14404
(略)

print-sentence-vectors モードでは、学習した分類器を指定して標準入力から適当な文章を渡すと、その文章の平均ベクトルを出力します。

$fasttext print-sentence-vectors model_cooking.bin 
How much does potato starch affect a cheese sauce recipe?
–0.017273 0.15645 -0.066831 0.16971 -0.0041063 0.21166 -0.21562 -0.23493 -0.081128 0.20374 0.0048495 0.1151 0.10965 -0.34534 -0.047381 -0.13311 0.24108 0.15479 -0.13747 -0.27368 -0.40173 0.12487 -0.10497 0.13571 0.1871 -0.36978 -0.15183 0.086557 0.075239 0.29059 -0.045744 0.012255 0.021844 0.015772 -0.1261 0.13502 -0.0162 -0.16172 0.11094 0.42339 -0.28965 0.03484 -0.063112 0.012793 -0.065129 -0.093443 0.030201 -0.045701 0.089005 -0.16689 -0.087488 -0.0070809 -0.017829 -0.016135 -0.062578 -0.083085 -0.23386 0.11845 -0.069138 0.075327 -0.14469 0.084418 -0.15946 0.18345 -0.2987 0.036661 -0.086342 0.10253 0.121 0.10054 -0.14697 0.020506 0.096789 -0.078027 0.0063817 -0.066075 0.089692 0.0079195 0.15794 0.026881 0.06317 0.17406 0.27988 -0.1954 -0.20847 -0.095213 0.27147 0.19933 -0.05129 0.21674 0.25814 0.19353 0.42013 -0.26088 0.14899 0.30501 0.045966 0.20921 0.13989 -0.21196
  • print-sentence-vectors モードを使用してみるとわかりますが、このモードで出力される文章の平均ベクトルは単純に文章中に含まれるベクトルの平均とは合わず、全ての文章に </s> なる単語が1つ足されていると考えると平均が合います。なので、</s> なる単語は意味を解釈するなら文章の区切りのようなもの(?)で、はたらきとしては文章ベクトルのオフセットのようなものだと思います。

複数のラベルが付いた文章を分類する(プレ処理とハイパーパラメータの変更)

先ほどの結果は Top-1 の全体の精度が約 14 %で感度が約 6 %でした。予測ラベルが本当に正しい割合が 14% とか、正解ラベルを検知できる割合が 6% とかはちょっと低いと思います。チュートリアルの Making the model better にならって、符号を切り離したり、全て小文字に揃えるプレ処理をしてみます。そうすると精度と感度が少しよくなります。

cd data
cat cooking.stackexchange.txt | sed -e "s/\([.\!?,'/()]\)/ \1 /g" | tr "[:upper:]" "[:lower:]" > cooking.preprocessed.txt
head -n 12404 cooking.preprocessed.txt > cooking.train
tail -n 3000 cooking.preprocessed.txt > cooking.valid
cd ..
$fasttext supervised -input data/cooking.train -output model_cooking
$fasttext test model_cooking.bin data/cooking.valid
N       3000
P@1     0.171
R@1     0.074

また、supervised モードの usage からわかるように学習のハイパーパラメータも色々あります。チュートリアルにしたがって学習率とエポック数を変更してみます。そうすると精度と感度がもっとぐっとよくなります。

$fasttext supervised -input data/cooking.train -output model_cooking -lr 1.0 -epoch 25
$fasttext test model_cooking.bin data/cooking.valid
N       3000
P@1     0.58
R@1     0.251

複数のラベルが付いた文章を分類する(オートチューニング)

ハイパーパラメータを変更すると精度がぐっとよくなることがわかりました。が、よいハイパーパラメータを探すのは面倒です。しかし幸いなことに、fastText にはオートチューニング機能が実装されました。

上のページにしたがって以下のように実行してみます。そうするとデフォルトで5分間探索するようです。

$fasttext supervised -input data/cooking.train -output model_cooking -autotune-validation data/cooking.valid
$fasttext test model_cooking.bin data/cooking.valid
N       3000
P@1     0.568
R@1     0.246

さっきより悪くなりました…。デフォルトでは f 値にフィッティングするみたいなんですが精度も感度も下がっているのでふつうにだめっぽいです。学習されたモデルの設定をダンプしてみると、まずベクトルの次元数が変わっています。

$fasttext dump model_cooking.bin args
dim 91
ws 5
epoch 100
minCount 1
neg 5
wordNgrams 3
loss softmax
model sup
bucket 2015548
minn 2
maxn 5
lrUpdateRate 100
t 0.0001

よくならないと悲しいので、ベクトルの次元数及び先ほど変更したハイパーパラメータは固定した上で、探索時間を 10 分にしてみます。そうするとさすがによくなりました。よかった。

$fasttext supervised -input data/cooking.train -dim 100 -lr 1.0 -epoch 25 -output model_cooking -autotune-validation data/cooking.valid -autotune-duration 600
$fasttext test model_cooking.bin data/cooking.valid
N       3000
P@1     0.608
R@1     0.263

このときのハイパーパラメータはこんな感じだったようです。

dim 100
ws 5
epoch 25
minCount 1
neg 5
wordNgrams 2
loss softmax
model sup
bucket 122181
minn 0
maxn 0
lrUpdateRate 100
t 0.0001

デフォルト値からずれているのは wordNgrams と bucket です。