nikkie-ftnextの日記

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

PyCon mini 静岡 Django入門トーク 補足資料 | Djangoが覆うWebアプリの世界 #pycon_shizu

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2020/02/29にPyCon mini 静岡でDjango入門トークをします。

発表準備した事項のうち、資料に盛り込みきれなかった部分を補足資料としてこの記事にまとめます。

目次

補足資料:Djangoが覆うWebアプリの世界 に至るまで

登壇準備をするに当たり、Django Girls Tutorialの内容について、Workshopなどでしている自分の説明を見直すとともに、「そもそもWebアプリとは何か」を説明できるようにインプットしました。
その中で、Webアプリの肝はHTTPで、DjangoがHTTPを覆っているから早く開発できるという理解に至りました。
「これは言ってみれば、DjangoでWebアプリを始めたあなたの知らないWebアプリの世界1だなあ」と思い、Webアプリについても登壇で言及しようとしました。
ですが、Django入門について話すだけでも30分はギリギリというフィードバック2をいただき、"あなたの知らないWebアプリの世界"はスライドから切り出すことを決めました。

誰のための補足資料?

PyCon mini 静岡のトークは、Web開発に興味がある方に、Djangoを紹介し、チュートリアルの見取り図を提供するものとなっています。
ですので、Djangoチュートリアルを終えている方や個人のアプリ開発で使い出している方にとっては、申し上げにくいのですが、今回のトークに新しい発見はあまりないと思います3
そのような方に伝えたい内容が、泣く泣くトークから切り出した"あなたの知らないWebアプリの世界"(=この補足資料)です。
Djangoチュートリアルを一度終えている方、Djangoの中身についてチュートリアルよりも詳しく知りたいという方は、ぜひ続きを読んでみてください。

それでは以下でDjangoが覆うWebアプリの世界を見ていきましょう。

リクエストとレスポンス

Webアプリは、Webという仕組みを使ったアプリケーションです。
インターネットに接続されたコンピュータ間での情報共有を実現するのがWebです(HTMLを使います)。
Webでコンピュータ同士が通信する際に、HTTPという取り決めに則ります。

HTTPに則った通信の登場人物には以下の2つがあります。

HTTP通信では、クライアントとサーバ間で以下のやり取りをします。

  • クライアントはサーバにHTTPリクエスを送信(Webブラウザに入力したデータも含みます)
  • サーバはHTTPリクエストに応じた処理をしてHTTPレスポンスを返却
  • クライアントはHTTPレスポンスに含まれるHTMLを解釈して表示

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

それでは、Djangoが覆うHTTP通信について、以下で見ていきましょう。
アプリケーションの4要素(URL設定、ビュー、テンプレート、モデル)がHTTPリクエストやレスポンスとどう関わるかについて見ていきます。

URL設定

URL設定はHTTPリクエストと関係します。
HTTPリクエストにはURIという項目があるのですが、URL設定はURIを処理します。

HTTPリクエストの一部

GET http://example.com/ HTTP/1.1
(以下略)

URIhttp://example.com/の部分です。

URL設定はHTTPリクエス4URIを見ます。
そして、URIのパスと一致するビューを呼び出します5

explain-how-django-works-for-beginner/urls.py at 8f1eaa0856cbfb499d05fdcdb2adb4fb997665ea · ftnext/explain-how-django-works-for-beginner · GitHub

urlpatterns = [
    # リクエスト中のURIが 127.0.0.1:8000/ のときは、
    # パスが''(空文字)となる。
    # blog/views.py の post_list 関数を呼び出す
    path('', views.post_list, name='post_list'),
    # リクエスト中のURIが 127.0.0.1:8000/ 以外の場合は
    # 設定していないのでエラー
]

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

ビュー

ビューは、HTTPリクエストともHTTPレスポンスとも関係します。
ビューはHTTPリクエストの情報を使って処理をします。
そして、処理の結果をHTTPレスポンスとして返します。

ビューが処理に使うHTTPリクエストの情報としては、例えば、HTTPメソッドがあります。

(再掲)HTTPリクエストの一部

GET http://example.com/ HTTP/1.1
(以下略)

HTTPメソッドはGETの部分です6

HTTPレスポンスには2つのポイントがあります。

ステータスコードはHTTPの通信結果を表します(処理が正常に行われたら200のように決まっています)。
メッセージボディはクライアントのWebブラウザに表示される部分です。

ビューが処理の結果をレスポンスとして返すことを実現する最も単純な方法はHttpResponseを返すことです。

explain-how-django-works-for-beginner/views.py at 8f1eaa0856cbfb499d05fdcdb2adb4fb997665ea · ftnext/explain-how-django-works-for-beginner · GitHub

def post_list(request):
    # 引数requestはHttpRequest(Django流のHTTPリクエストの扱い方)
    # request.methodでHTTPメソッドを確認できる
    if request.method == 'GET':
        # メッセージボディを指定して,HttpResponseを返す(→ブラウザに表示される)
        return HttpResponse('GETリクエストへのレスポンスです')
    # 以下略

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

このようにして、URL設定とビューがあればHTTPに則ってリクエストにレスポンスを返すことができます7

テンプレート

URL設定とビューで返すHTTPレスポンスは単なる文字列でHTMLではありませんでした。
HTTPレスポンスとしてHTMLを使うにはテンプレートの出番です。
HTTPレスポンスのメッセージボディにHTMLを文字列として含めます

explain-how-django-works-for-beginner/views.py at 1db8d0827afac7514725db489fc21efcf82bf25d · ftnext/explain-how-django-works-for-beginner · GitHub

def post_list(request):
    # 指定したテンプレート(HTML)をメッセージ本文に入れた
    # HTTPレスポンス(HttpResponse)を、render関数で作って返す
    return render(request, 'blog/post_list.html', {})
    # (データを埋め込まない場合)URL設定に対してテンプレートが対応する形になる

以下のように連携します。

  1. URL設定がパスに対応するビューを呼び出し
  2. ビューが処理
  3. ビューがテンプレートを取得し、処理結果を埋め込んでHTTPレスポンスとして返す

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

render関数の動きを理解するには、Django公式ドキュメントの投票アプリチュートリアルがオススメです。
render関数がHttpResponseを返していることが分かります。

モデル

テンプレートを使うことでパスに対して決まったHTMLをHTTPレスポンスとして返せるようになりました8
次はデータをテンプレートから切り出して考えます。
データをデータベースで管理し、リクエストに応じてデータを取り出します。
取り出したデータをテンプレートの中に埋め込んでHTTPレスポンスを作成します。
また、HTTPリクエストの情報をもとにデータを保存することも可能です。

データベースからデータの取得・保存に使うのが、モデルです。

実装は、モデルを使って取得したデータを辞書でrenderの第3引数に渡します(仕組みは、テンプレートのところで紹介した投票アプリチュートリアルをどうぞ!)。

explain-how-django-works-for-beginner/views.py at 3b0133546c8a8a565de5a0427f164b666b5378e0 · ftnext/explain-how-django-works-for-beginner · GitHub

def post_list(request):
    # ブログ投稿のうち、published_date(公開日)が現在より前のものを取得し。
    # 公開日の昇順(以前に公開されたものほど上)に並べ替える
    posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')
    # 取得した投稿データpostsをテンプレートでpostsという名前で扱えるように渡す
    return render(request, 'blog/post_list.html', {'posts': posts})

以下のように連携します。

  1. URL設定がパスに対応するビューを呼び出し
  2. ビューが処理(モデルを使ってデータの取得や保存をする
  3. ビューがテンプレートを取得し、処理結果を埋め込んでHTTPレスポンスとして返す

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

まとめ

Webアプリを支えるHTTPリクエストとHTTPレスポンスという観点から、Djangoが覆っている部分に光を当ててみました。
Django Girls Tutorialを何周かしてDjangoに慣れてきたら、「Webアプリとは何か」を学んでみると視野が広がるのではないでしょうか。

Djangoを使っていくとDjangoの使い方を覚えるような状況にもなります。
ですが、Djangoの使い方を覚えるだけで済むというのは、DjangoがHTTP通信を覆っていて、開発者がHTTPリクエストやHTTPレスポンスを直接扱わなくても済むから可能になっているのだと気づきました。
また、HTTP通信は共通化しやすいと思うので、Djangoが覆っているのも納得です。

最後に今回Webアプリについて学ぶ際に参考になった書籍を紹介します。

10年ほど前の本ですが、HTTP通信の説明は現在でも通用する(Webの大元は10年前から変わっていない)と思いました。

補足資料は以上です。


  1. PyCon mini 静岡のテーマ「あなたの知らないPython」になぞらえています

  2. この視点は登壇者練習会で岡崎さんからいただきました。ありがとうございます

  3. 裏のcmath複素数を扱うライブラリ)トークをどうぞ!PyCon mini Shizuoka - 君はcmathを知っているか

  4. 前提として、Djangoで作られたWebアプリが受け取ったHTTPリクエストはDjango流のオブジェクトに変換されています。

  5. ビューにはHTTPリクエストを表すオブジェクトが渡されます

  6. 今回は詳細には立ち入りませんが、URIへのデータの送信の仕方を表すもので、GETの他にPOSTなど何種類かあります

  7. このことを登壇ではURLとビューで最小限のWebアプリが作れると表現しています

  8. パスpost/1/には1.htmlを、post/2/には2.htmlを返すというのは一案ですが、post/10/まで用意するだけでも大変です。そこで、post/整数/に対してtemplate.htmlを返すようにし、中身のデータを1番のデータ、2番のデータ、、、というようにWebアプリとは別で管理します(→データベース)。

『入門 自然言語処理』5章を写経し、ルールベースと訓練、2種類の自動タグ付けの方法を学びました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

2/3の週から自然言語処理の基礎固めとして『入門 自然言語処理』に取り組んでいます。

入門 自然言語処理

入門 自然言語処理

今週は5章「単語の分類とタグ付け」からタガーを使った自動タグ付けについてアウトプットします。
ここでやりたいことは、トークンに対してタグ(=品詞)をつけること。
キーワードはこちら:

  • バックオフ
  • Nグラムモデル

『入門 自然言語処理』英語版(かつ、Python 3対応版)は公開されています:

目次

動作環境

先週のWordCloudと同じ環境です。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ python -V  # venvによる仮想環境を使用
Python 3.7.3
$ pip list  # grepを使って抜粋して表示
beautifulsoup4   4.8.2
ipython          7.12.0
matplotlib       3.1.3
nltk             3.4.5
wordcloud        1.6.0

brown コーパス

5章で用いるコーパスは「brown」なるもの。

In [1]: import nltk

In [3]: print(nltk.corpus.brown.readme())
BROWN CORPUS

A Standard Corpus of Present-Day Edited American
English, for use with Digital Computers.

by W. N. Francis and H. Kucera (1964)
Department of Linguistics, Brown University
Providence, Rhode Island, USA

Revised 1971, Revised and Amplified 1979

http://www.hit.uib.no/icame/brown/bcm.html

Distributed with the permission of the copyright holder,
redistribution permitted.

カテゴリがいくつかあり、5章ではnews (A?) や learned (J)、editorial (B)を用いました1

brownコーパスのデータの取得方法はいくつかあるのでまとめます(前提としてfrom nltk.corpus import brownしているとします):

  • brown.wordsトークンからなるリスト
    • len1161192
    • 例:['The', 'Fulton', 'County', 'Grand', 'Jury', ..., 'boucle', 'dress', 'was', 'stupefying', '.']
  • brown.sents:文(sentences)を表すトークンのリストからなるリスト
    • len57340
    • 文に当たる部分:['The', 'Fulton', 'County', ..., 'took', 'place', '.']
  • brown.tagged_words(トークン, 品詞)というタプルからなるリスト
    • len1161192
    • 例:[('The', 'AT'), ('Fulton', 'NP-TL'), ('County', 'NN-TL'), ('Grand', 'JJ-TL'), ('Jury', 'NN-TL'), ...]
  • brown.tagged_sents:文に相当する、(トークン, 品詞)というタプルのリストからなるリスト
    • len57340
    • brown.tagged_wordsの返り値の形式で、brown.sentsの返り値のように文ごとに分かれている

これらのメソッドはcategories引数で取得する文書のカテゴリを指定できます。

ルールを元にした自動タグ付け

自動タグ付けの手法は2つあります。
ルールベースのタガー(タグ付け器)を作る方法から学びました。

1. DefaultTagger

nltk.tag.sequential.DefaultTagger2を使い、どんなトークンにも指定のタグ(以下の例ではNN)を付けます。

In [126]: raw = 'I do not like green eggs and ham, I do not like them Sam I am!'

In [127]: tokens = nltk.word_tokenize(raw)

In [128]: default_tagger = nltk.DefaultTagger('NN')

In [129]: default_tagger.tag(tokens)
Out[129]:
[('I', 'NN'),
 ('do', 'NN'),
 ('not', 'NN'),
 ('like', 'NN'),
 ('green', 'NN'),
 ('eggs', 'NN'),
 ('and', 'NN'),
 ('ham', 'NN'),
 (',', 'NN'),
 ('I', 'NN'),
 ('do', 'NN'),
 ('not', 'NN'),
 ('like', 'NN'),
 ('them', 'NN'),
 ('Sam', 'NN'),
 ('I', 'NN'),
 ('am', 'NN'),
 ('!', 'NN')]

brownコーパスでタグ付けの性能を確認する3と、正解は8分の1程度です。

In [121]: from nltk.corpus import brown

In [122]: brown_tagged_sents = brown.tagged_sents(categories='news')

In [130]: default_tagger.evaluate(brown_tagged_sents)
Out[130]: 0.13089484257215028

2. RegexpTagger

正規表現とタグの対応ルールのタプルで表し、そのリストをnltk.tag.sequential.RegexpTaggerに渡します。

In [133]: patterns = [
     ...:     (r'.*ing$', 'VBG'),
     ...:     (r'.*ed$', 'VBD'),
     ...:     (r'.*es$', 'VBZ'),
     ...:     (r'.*ould$', 'MD'),
     ...:     (r'.*\'s$', 'NN$'),
     ...:     (r'.*s$', 'NNS'),
     ...:     (r'^-?[0-9]+(.[0-9]+)?$', 'CD'),
     ...:     (r'.*', 'NN'),
     ...: ]

In [134]: regexp_tagger = nltk.RegexpTagger(patterns)

性能は

In [136]: regexp_tagger.evaluate(brown_tagged_sents)
Out[136]: 0.20326391789486245

ドキュメントに記載されていたパターンを追加すると、性能は向上しました。

In [139]: patterns = [
     ...:     (r'.*ing$', 'VBG'),
     ...:     (r'.*ed$', 'VBD'),
     ...:     (r'.*es$', 'VBZ'),
     ...:     (r'.*ould$', 'MD'),
     ...:     (r'.*\'s$', 'NN$'),
     ...:     (r'.*s$', 'NNS'),
     ...:     (r'^-?[0-9]+(.[0-9]+)?$', 'CD'),
     ...:     (r'(The|the|A|a|An|an)', 'AT'),
     ...:     (r'.*able$', 'JJ'),
     ...:     (r'.*ness$', 'NN'),
     ...:     (r'.*ly$', 'RB'),
     ...:     (r'.*', 'NN'),
     ...: ]

In [140]: regexp_tagger = nltk.RegexpTagger(patterns)

In [141]: regexp_tagger.evaluate(brown_tagged_sents)

Out[141]: 0.2889591662191459

3. ルックアップタガー

brownコーパス最頻出トークン100語のタグ付けを参照(look up)するタガーです。

In [142]: fd = nltk.FreqDist(brown.words(categories='news'))

In [143]: cfd = nltk.ConditionalFreqDist(brown.tagged_words(categories='news'))

In [144]: most_freq_words = fd.most_common(100)

In [146]: most_freq_words[:5]
Out[146]: [('the', 5580), (',', 5188), ('.', 4030), ('of', 2849), ('and', 2146)]

In [147]: likely_tags = dict((word, cfd[word].max()) for word, _ in most_freq_words)

In [148]: list(likely_tags)[:5]
Out[148]: ['the', ',', '.', 'of', 'and']

In [149]: likely_tags['the']
Out[149]: 'AT'

nltk.probability.FreqDistcollections.Counterを継承しています。
ここでは、トークンごとの出現回数を数え、上位100件をmost_freq_wordsとしました。

nltk.probability.ConditionalFreqDistcollections.defaultdictを継承しています。
トークンごとに品詞の出現回数を数えました(例:トークンに対して品詞Aが◯回、品詞Bが△回出現)。
トークンごとに一番出現回数が多い品詞を対応付けてlikely_tagsという辞書を作っています。

likely_tagsトークンに対応する品詞の辞書)を参照するタガーをnltk.tag.sequential.UnigramTaggerで作ります。
model引数に参照する辞書を渡します。

In [150]: baseline_tagger = nltk.UnigramTagger(model=likely_tags)

性能確認です。

In [151]: baseline_tagger.evaluate(brown_tagged_sents)
Out[151]: 0.45578495136941344

ルックアップタガーは知らないトークンについてタグ付けできません。
そこで、タグ付けできない場合は別のタガー(ここではDefaultTagger)を使うことにします。
タグを付けられない場合に別のタガーを使うことをバックオフと言うそうです。

backoff引数にDefaultTaggerを渡します。

In [154]: baseline_tagger = nltk.UnigramTagger(model=likely_tags, backoff=nltk.DefaultTagger('NN'))

性能が上がりました!

In [155]: baseline_tagger.evaluate(brown_tagged_sents)
Out[155]: 0.5817769556656125

likely_tagsという辞書がある程度広い範囲をカバーするまで、タグ付け結果の正解率が増えていきます。
書籍中のグラフ4を元に、上位2000語で試してみました。

In [156]: lt = dict((word, cfd[word].max()) for word, _ in fd.most_common(2000))

In [157]: baseline_tagger2 = nltk.UnigramTagger(model=lt, backoff=nltk.DefaultTagger('NN'))

In [158]: baseline_tagger2.evaluate(brown_tagged_sents)
Out[158]: 0.7775424150207848

DefaultTaggerから比べて、ルックアップタガー+バックオフのDefaultTaggerはだいぶ性能が上がりました。

訓練を元にした自動タグ付け

ルールベースとは別の方法として、訓練でタガーを作る方法も学びました。

未知のテキストに対する評価をするために、訓練用とテスト用にテキストを分けます(これは機械学習モデルの性能評価と共通の考え方ですね)。

In [162]: size = int(len(brown_tagged_sents) * 0.9)

In [163]: size
Out[163]: 4160

In [165]: train_sents = brown_tagged_sents[:size]

In [166]: test_sents = brown_tagged_sents[size:]

1. UnigramTagger

ルックアップタガーと同様にUnigramTaggerを使います。
ルックアップタガーとの違いは、文単位の(トークン, 品詞)のリストのリストをtrain引数に渡すことです(第1引数なのでtrain=と明示しなくても渡せます)。

In [167]: unigram_tagger = nltk.UnigramTagger(train_sents)

train_sentsを用いて、最も多く登場した品詞でタグ付けするように訓練されました。
性能を確認しましょう。

In [168]: unigram_tagger.evaluate(test_sents)
Out[168]: 0.8121200039868434

2. Nグラムタガー(BigramTagger)

UnigramTaggerがタグ付けに使う情報はトークンだけです。
それに対して、文脈の情報を使うことを考えます。
Nグラムタガーはタグを付けたいトークンの他に、トークンの前のN-1語の品詞という情報も使います。

N=2のBigramTaggerについて見てみましょう(NLTK ドキュメント)。

In [169]: bigram_tagger = nltk.BigramTagger(train_sents)

性能は。。。

In [172]: bigram_tagger.evaluate(test_sents)
Out[172]: 0.10206319146815508

上がると思いきや、ガクッと下がりました。
これは、

  1. 未知のトークンに対してタグ付けができない
  2. 未知という品詞 + 既知のトークン という組合せが訓練データにないために未知のトークンに続く既知のトークンにもタグ付けができない

ためだそうです。

知らないトークンにタグ付けできないというのはルックアップタガーでも見た事象ですね。
そこで、再度バックオフの出番です。

3. バックオフでタガーを組み合わせる

以下のように組合せます:

  1. BigramTaggerでタグ付け
  2. 1ができない場合、UnigramTaggerでタグ付け
  3. 2ができない場合、DefaultTaggerでタグ付け

実装は、各タガーのbackoff引数に指定する形になります。

In [173]: t0 = nltk.DefaultTagger('NN')

In [174]: t1 = nltk.UnigramTagger(train_sents, backoff=t0)

In [175]: t2 = nltk.BigramTagger(train_sents, backoff=t1)

In [176]: t2.evaluate(test_sents)
Out[176]: 0.8452108043456593

BigramTagger単体と比べて性能が上がりました。
これはUnigramTaggerと比べても上がっています。

TrigramTagger (N=3) も試してみたところ、性能は同程度という結果でした。

In [177]: t3 = nltk.TrigramTagger(train_sents, backoff=t2)

In [178]: t3.evaluate(test_sents)
Out[178]: 0.843317053722715

タガーの取り扱い

保存

pickle.dumpを使ってファイルに保存します5

In [179]: from pickle import dump

In [180]: with open('t2.pkl', 'wb') as output:
     ...:     dump(t2, output, -1)
     ...:

コマンドラインの別ウィンドウ(すなわち別のPythonプロセス)で読み込めることを確認します。

>>> from pickle import load
>>> with open('t2.pkl', 'rb') as input:
...     tagger = load(input)
...
>>> text = """The board's action shows what free enterprise
...     is up against in our complex maze of regulatory laws ."""
>>> tokens = text.split()
>>> tagger.tag(tokens)
[('The', 'AT'), ("board's", 'NN$'), ('action', 'NN'), ('shows', 'NNS'), ('what', 'WDT'), ('free', 'JJ'), ('enterprise', 'NN'), ('is', 'BEZ'), ('up', 'RP'), ('against', 'IN'), ('in', 'IN'), ('our', 'PP$'), ('complex', 'JJ'), ('maze', 'NN'), ('of', 'IN'), ('regulatory', 'NN'), ('laws', 'NNS'), ('.', '.')]

性能確認

nltk.metrics.confusionmatrix.ConfusionMatrixで混同行列が確認できます。

In [181]: test_tags = [tag for sent in brown.sents(categories='editorial') for word, tag in t2.tag(sent)]

In [182]: gold_tags = [tag for word, tag in brown.tagged_words(categories='editorial')]

In [184]: nltk.ConfusionMatrix(gold_tags, test_tags)
Out[184]: <ConfusionMatrix: 52073/61604 correct>

まとめ

品詞の自動タグ付けについて以下を学びました。

  • 自動タグ付け=タガーを作る
  • タガーの作り方には、ルールベースの手法と訓練による手法がある
  • 訓練による手法の1つ、Nグラムモデルは、トークンの前の語の品詞の情報もタグ付けに使う
  • 複数のタガーを組み合わせる:バックオフ(例:Bigram, Unigram, DefaultTagger)

感想

品詞タグ付けに訓練という機械学習的なアプローチが使われているというのは初めて知りました。
3章のトークン化と5章の品詞タグ付けの話で、PyCon JP 2019で聞いたnagisaの話と繋がりが見え始めました。

英語テキストについて今回学んだことが、日本語の場合にどこまで当てはまるのか気になっています。
というわけで、次回は『入門 自然言語処理』12章「Pythonによる日本語自然言語処理」に取り組む予定です。


  1. nltk.download('brown')をしていれば、~/nltk_data/corpora/brown にファイルがあり、直接見られます

  2. nltkの各モジュールはnltk.awesome_moduleのようにnltkパッケージ直下から参照できるのですが、これはnltk/__init__.py*を使ってimportしているために実現されていました(ref

  3. evaluateメソッドは、文単位の(トークン, 品詞)タプルのリストを集めたリストを受け取ります ref: https://www.nltk.org/api/nltk.tag.html#nltk.tag.api.TaggerI.evaluate

  4. ref: http://www.nltk.org/images/tag-lookup.png

  5. protocolは0~5までの6段階(ref: ドキュメント)ですが、protocol引数に-1が渡されています。この場合、利用できる中で一番高い段階のprotocolとなるそうです ref: Pickling and Unpickling in python Explained - DEV Community 👩‍💻👨‍💻

『入門 自然言語処理』5章から、英語テキストでも品詞分類できると知った私は、特定の品詞を取り出したWordCloudを試してみました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

先週から自然言語処理の基礎固めとして『入門 自然言語処理』に取り組んでいます。

入門 自然言語処理

入門 自然言語処理

今週は5章「単語の分類とタグ付け」の冒頭部分、品詞タグ付けの紹介から、英語のテキストについて品詞タグ付けを試してみました。

目次

前回までのブログ駆動開発!との関連

先日 janomeチュートリアル1の中で、「風の又三郎」(日本語テキスト)から名詞と動詞を取り出して、WordCloudを作りました。

チュートリアルは、英語テキストの例として、『不思議の国のアリス』でもWordCloudを作っています2

『入門 自然言語処理3で、英語テキストについての品詞タグ付け(以下、タグ付け)を知りました。
「『風の又三郎』の例で品詞を名詞と動詞に絞ったのと同じように、『不思議の国のアリス』でも品詞を絞れるのでは?」と着想し、試してみました。
不思議の国のアリス』の英語テキストから、名詞と動詞を使ってWordCloudを作ります

動作環境

先週と同じ環境に、WordCloud作成に必要なパッケージを追加しました。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ python -V  # venvによる仮想環境を使用
Python 3.7.3
$ pip list  # grepを使って抜粋して表示
beautifulsoup4   4.8.2
ipython          7.12.0
matplotlib       3.1.3  # 追加
nltk             3.4.5
wordcloud        1.6.0  # 追加

WordCloudを作れるよう、janomeチュートリアルに沿った環境構築をします。

$ unzip ~/Downloads/ipagp00303.zip -d .
$ wget https://raw.githubusercontent.com/mocobeta/janome-tutorial/master/hands-on/data/alice_utf8.txt .

フォルダ配置

.
├── alice_utf8.txt
├── alice_word_cloud.py
├── env
├── ipagp00303
└── result  # 作成したWordCloudの画像を置く

nltk を使ったタグ付け

nltk.word_tokenize したテキスト(トークンのリスト)をnltk.tag.pos_tagでタグ付けできます。

Use NLTK’s currently recommended part of speech tagger to tag the given list of tokens.

今回使われたタガーは averaged_perceptron_tagger のようです(例のごとくnltk.downloadが必要でした)

以下は、同音異義語が登場する例です。

In [8]: text = nltk.word_tokenize("They refuse to permit us to obtain the refuse permit")

In [9]: nltk.pos_tag(text)
Out[9]:
[('They', 'PRP'),
 ('refuse', 'VBP'),
 ('to', 'TO'),
 ('permit', 'VB'),
 ('us', 'PRP'),
 ('to', 'TO'),
 ('obtain', 'VB'),
 ('the', 'DT'),
 ('refuse', 'NN'),
 ('permit', 'NN')]

結果から、

  • 名詞はNNで始まる
  • 動詞はVBで始まる

ようです。
書籍 例5-1 のプログラムを見たところ、これらはNNやVBで始まると仮定してよさそうだったので、続きを考えていきました(時間の制約により掘り下げられていません)。

ソースコード

import matplotlib.pyplot as plt
import nltk
from wordcloud import WordCloud


SEED = 42


def show_wordcloud(text, save_path):
    if isinstance(text, list):
        text = ' '.join(text)
    wordcloud = WordCloud(
        font_path='ipagp00303/ipagp.ttf',
        background_color='white',
        width=1024,
        height=674,
        random_state=SEED,
        collocations=False).generate(text)
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.axis('off')
    plt.savefig(save_path)


if __name__ == '__main__':
    with open('alice_utf8.txt', encoding='utf8') as f:
        concatenated_text = f.read()
        concatenated_text = concatenated_text.lower()  # 正規化
    show_wordcloud(concatenated_text, 'result/orig_alice_cloud.png')

    text = nltk.word_tokenize(concatenated_text)
    print(f'tokenized: {len(text)}')
    show_wordcloud(text, 'result/tokenized.png')

    tagged = nltk.pos_tag(text)
    nouns_and_verbs = [
        word for word, tag in tagged if tag.startswith(('NN', 'VB'))]
    print(f'tagged: {len(nouns_and_verbs)}')
    show_wordcloud(nouns_and_verbs, 'result/nouns_and_verbs.png')

作成されたWordCloud

上記のスクリプトを実行すると、3つのWordCloudができます。

  1. 元のファイルの全文から作ったWordCloud
  2. word_tokenizeした後の単語から作ったWordCloud
  3. 2から名詞と動詞を取り出して作ったWordCloud

1.元のファイルの全文から作ったWordCloud

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

2. word_tokenizeした後の単語から作ったWordCloud

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

n't という単語が現れているところにトークン化の効果が感じられます。

WordCloud.generateには、自然言語のテキスト(a natural text)を渡す必要があるため、単語のリストを半角スペースでjoinして自然言語のテキストのように見せました。

3. 2から名詞と動詞を取り出して作ったWordCloud

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

WordCloudに含まれる単語数は

  • トークン化で12460語
  • 名詞と動詞を取り出して4106語

と変化しました。
queen などの名詞が2より大きく表示されているところに効果を感じます。
風の又三郎の例のように、見ただけで内容が分かるようにしたいので、工夫の余地はありそうですね。

感想

今週平日は別に締切のあったタスクを進め、土日はOOC 2020の当日スタッフをし、いま、OOCの達成感と疲労の中でたびたび意識を飛ばしつつ、この記事を書いています。
これまでの週に比べると短縮版ではありますが、「『風の又三郎』の例でやったことは日本語に限らず英語でもできるんだ」という気づきがありました。

入門 自然言語処理』を読み進めることで、言語の違いによらない、自然言語処理の抽象的な方法を学べそうという印象です。
土曜日にお別れの会が執り行われたPython2系ですが、『入門 自然言語処理』を通して引き続き思い出作りをしていきます。

課題

  • startswith(('NN', 'VB'))よりも深堀りした名詞・動詞の取り出し方
    • そのために5章をもっと読む
  • 3章のステミングやレマタイズを取り入れる
  • WordCloudの余白は、matplotlibのコードのどこが原因?

『入門 自然言語処理』3章をPython 3で写経し、テキスト処理の前処理であるステミングとトークン化についてインプットしました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

今週からは自然言語処理の基礎固めとして『入門 自然言語処理』に取り組んでいきます。

入門 自然言語処理

入門 自然言語処理

今週は3章から、英語のテキストについて、以下のトピックを扱います。

目次

『入門 自然言語処理』とは

2010年発行のオライリー本です。
NLTKというパッケージを使った自然言語処理について書かれています。

この本のコードはPython2系で書かれているので、Python3系に書き直しつつ取り組んでいくことになります1
Python3系対応の英語版がオンラインで公開されていたので、コードに詰まったらオンライン版を参考にしながら進めました。

この本については「禁書にすべき」という声も上がっています2

動作環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ python -V  # venvによる仮想環境を使用
Python 3.7.3
$ pip list  # grepを使って抜粋して表示
beautifulsoup4   4.8.2
ipython          7.12.0
nltk             3.4.5

NLTKのLookupError

『入門 自然言語処理』のコードを写経していく中でLookupErrorにたびたび出会いました。

In [16]: from nltk.tokenize import word_tokenize

In [17]: tokens = word_tokenize(raw)
---------------------------------------------------------------------------
LookupError                               Traceback (most recent call last)
...(省略)...
LookupError:
**********************************************************************
  Resource punkt not found.
  Please use the NLTK Downloader to obtain the resource:

  >>> import nltk
  >>> nltk.download('punkt')
(以下省略)

エラーメッセージで案内されているように、nltk.downloadを使って必要なデータをダウンロードすることでLookupErrorは解消します3
ダウンロードしたデータは$HOME/nltk_dataに置かれました。

$ tree -L 1 $HOME/nltk_data
/Users/.../nltk_data
├── corpora
└── tokenizers

2 directories, 0 files

自然言語処理のパイプライン(3.1.8より)

3章は自然言語処理の流れを扱う章です(本記事ではステミングやトークン化に注力します)。
自然言語処理で扱うデータの流れは以下のようになると理解しました4

  1. まずbytesを扱う
    • urllib.request.urlopenでWeb上のリソースを読み込む
  2. bytesdecodeしてstr
    • HTMLの場合はBeautifulSoupのget_textメソッドでHTMLタグを除く
    • ヘッダーやフッターを除き、必要な部分を抜き出すstrfindrfindメソッド)
  3. strlistに(要素はstr
  4. トークンのリストからnltk.Text5
  5. 正規化し語彙を構築

ステミングやトークン化は3のステップに関わります。

ステミング

ステミングとは、単語から語幹を取り出すことです。
ステミングにより、例えば、名詞の単数形と複数形や、動詞の原形と活用形を同一として扱えると理解しています。
『入門 自然言語処理』では正規表現を使った例から始めてNLTKのステマーが紹介されました。

ステミングの対象の語句は、str.lowerメソッドで小文字に正規化するのがよさそうに思います。

正規表現を利用してステマーを作る

以下の関数から始めて正規表現を導入していきます。

In [193]: def stem(word):
     ...:     for suffix in ['ing', 'ly', 'ed', 'ious', 'ies', 'ive', 'es', 's', 'ment']:
     ...:         if word.endswith(suffix):
     ...:             return word[:-len(suffix)]
     ...:     return word
     ...:

In [194]: stem('processing')
Out[194]: 'process'

In [198]: stem('processes')
Out[198]: 'process'

文字列がing, ly, ed, ious, ies, es, s, mentいずれかで終わる場合、該当する文字列とそれ以前の部分を返します。
例えばprocessingを渡した場合、(process)(ing)正規表現にマッチするので、()の機能でキャプチャされて、re.findallで返されます。

In [205]: re.findall(r'^(.*)(ing|ly|ed|ious|ies|ive|es|s|ment)$', 'processing')
Out[205]: [('process', 'ing')]

|(パイプ、論理和)は左からマッチさせるので、s|esのようなパターンではsが削除されてeが残るということが起こりえますね。

この正規表現には問題があり、processesをstem関数のように処理できません。

In [206]: re.findall(r'^(.*)(ing|ly|ed|ious|ies|ive|es|s|ment)$', 'processes')
Out[206]: [('processe', 's')]

理由は*が貪欲マッチ((process)(es)よりも(processe)(s)の方が*にマッチする部分が長いので選ばれる)だからです。
*?とすることでこの挙動は解決します6

'*' 、 '+' 、および '?' 修飾子は全て 貪欲 (greedy) マッチで、できるだけ多くのテキストにマッチします。この挙動が望ましくない時もあります。(中略)修飾子の後に ? を追加すると、 非貪欲 (non-greedy) あるいは 最小 (minimal) のマッチが行われ、できるだけ 少ない 文字にマッチします。

In [207]: re.findall(r'^(.*?)(ing|ly|ed|ious|ies|ive|es|s|ment)$', 'processes')
Out[207]: [('process', 'es')]

あとはing, ly, ed, ious, ies, es, s, mentのいずれでも終わらない文字列向けに?を追加します。

In [209]: re.findall(r'^(.*?)(ing|ly|ed|ious|ies|ive|es|s|ment)?$', 'language')
Out[209]: [('language', '')]

こうして、正規表現を使ったステマーが完成しました!

In [210]: def stem(word):
     ...:     regexp = r'^(.*?)(ing|ly|ed|ious|ies|ive|es|s|ment)?$'
     ...:     stem, suffix = re.findall(regexp, word)[0]
     ...:     return stem
     ...:

挙動に

  • ingやedが消えるのでlikeのing系 likingが元に戻らない(likになる)
  • isやbasisなどのsを落とす

というイケていない点があるものの、正規表現ステマーができました。

NLTK組み込みのステマーを利用する

『入門 自然言語処理』は、NLTK組み込みのステマーを使うことを勧めています7
理由は、組み込みのステマーは幅広く例外を扱えるためだそうです。

ステマー適用対象のトークンのリストを用意します。

In [223]: raw = """DENNIS: Listen, strange women lying in ponds distributing swords
     ...: is no basis for a system of government.  Supreme executive power derives from
     ...: a mandate from the masses, not from some farcical aquatic ceremony."""
     ...:

In [224]: tokens = nltk.word_tokenize(raw)

2つのステマーが紹介されました。

In [225]: porter = nltk.PorterStemmer()

In [226]: lancaster = nltk.LancasterStemmer()

In [227]: [porter.stem(t) for t in tokens]
Out[227]:
['denni',
 ':',
 'listen',
 ',',
 'strang',
 'women',
 'lie',
 'in',
 'pond',
 'distribut',
 'sword',
 'is',
 'no',
 'basi',
 'for',
 'a',
 'system',
 'of',
 'govern',
 '.',
 'suprem',
 'execut',
 'power',
 'deriv',
 'from',
 'a',
 'mandat',
 'from',
 'the',
 'mass',
 ',',
 'not',
 'from',
 'some',
 'farcic',
 'aquat',
 'ceremoni',
 '.']

In [228]: [lancaster.stem(t) for t in tokens]
Out[228]:
['den',
 ':',
 'list',
 ',',
 'strange',
 'wom',
 'lying',
 'in',
 'pond',
 'distribut',
 'sword',
 'is',
 'no',
 'bas',
 'for',
 'a',
 'system',
 'of',
 'govern',
 '.',
 'suprem',
 'execut',
 'pow',
 'der',
 'from',
 'a',
 'mand',
 'from',
 'the',
 'mass',
 ',',
 'not',
 'from',
 'som',
 'farc',
 'aqu',
 'ceremony',
 '.']

書籍中で指摘されたlying(7番目の単語)のステミングの比較から、PorterStemmerを使いたいと思っています。
また、basis, farcical, aquaticを見ると、PorterStemmerの方が文字の減りが少ないです。

レマタイズ

見出し語化のことで、トークンを辞書に載っている語形にします。

WordNetLemmatizer

In [229]: wnl = nltk.WordNetLemmatizer()

In [232]: [wnl.lemmatize(t) for t in tokens]
Out[232]:
['DENNIS',
 ':',
 'Listen',
 ',',
 'strange',
 'woman',
 'lying',
 'in',
 'pond',
 'distributing',
 'sword',
 'is',
 'no',
 'basis',
 'for',
 'a',
 'system',
 'of',
 'government',
 '.',
 'Supreme',
 'executive',
 'power',
 'derives',
 'from',
 'a',
 'mandate',
 'from',
 'the',
 'mass',
 ',',
 'not',
 'from',
 'some',
 'farcical',
 'aquatic',
 'ceremony',
 '.']

ステマーが処理していなかったwomenをwomanに変更できています。 一方、lyingはそのままです。

レマタイザは遅いそうなので、処理にかかる時間の優先度によるかと思いますが、WordNetLemmatizer→PorterStemmerの順で試してみたいと思いました。

トークン化

文字列をトークンに分割することです。
トークンとは、「言語データの一部を構成する識別可能な言語学上の単位」だそうです。

正規表現を使ったトークン化

最も単純なトークン化は、空白文字でテキストを分割することです。
r'\s+'は「空白文字の1回以上の繰り返し」です。
空白文字の1回以上の繰り返しというパターンで文字列を分割します(re.split)。

In [234]: raw = """'When I'M a Duchess,' she said to herself, (not in a very hopeful tone
     ...: though), 'I won't have any pepper in my kitchen AT ALL. Soup does very
     ...: well without--Maybe it's always pepper that makes people hot-tempered,'..."""

In [238]: re.split(r'\s+', raw)
Out[238]:
["'When",
 "I'M",
 'a',
 "Duchess,'",
 'she',
 'said',
 'to',
 'herself,',
 '(not',
 'in',
 'a',
 'very',
 'hopeful',
 'tone',
 'though),',
 "'I",
 "won't",
 'have',
 'any',
 'pepper',
 'in',
 'my',
 'kitchen',
 'AT',
 'ALL.',
 'Soup',
 'does',
 'very',
 'well',
 'without--Maybe',
 "it's",
 'always',
 'pepper',
 'that',
 'makes',
 'people',
 "hot-tempered,'..."]

単語に(や'といった記号が含まれてしまうのに対応するため、re.splitre.findallに変え、r'\w+'(=r'[a-zA-Z0-9_]+'に該当するものを取り出すようにします。

In [242]: re.findall(r'\w+', raw)
Out[242]:
['When',
 'I',
 'M',
 'a',
 'Duchess',
 'she',
 'said',
 'to',
 'herself',
 'not',
 'in',
 'a',
 'very',
 'hopeful',
 'tone',
 'though',
 'I',
 'won',
 't',
 'have',
 'any',
 'pepper',
 'in',
 'my',
 'kitchen',
 'AT',
 'ALL',
 'Soup',
 'does',
 'very',
 'well',
 'without',
 'Maybe',
 'it',
 's',
 'always',
 'pepper',
 'that',
 'makes',
 'people',
 'hot',
 'tempered']

正規表現を拡張していきます。
まずIt'sをItと'sに分けられるようにします。
r'\w+'[a-zA-Z0-9_]の1回以上の繰り返し)に一致しなければ、r'\S\w*'(空白文字以外の1文字とr'\w'0文字以上)に一致するものを探します。

In [243]: re.findall(r'\w+', "It's show time.")
Out[243]: ['It', 's', 'show', 'time']

In [244]: re.findall(r'\w+|\S\w*', "It's show time.")
Out[244]: ['It', "'s", 'show', 'time', '.']

--...に対応できるように正規表現を拡張します。

In [251]: re.findall(r"\w+(?:[-']\w+)*|'|[-.(]+|\S\w*", "It's show time. '--' ... (hot-tempered)")
Out[251]: ["It's", 'show', 'time', '.', "'", '--', "'", '...', '(', 'hot-tempered', ')']

追加した正規表現パターンにより

  • --...を抜き出せる(r'[-.(]+'
  • シングルクォートを抜き出せる(r"'"

正規表現中の()はキャプチャ機能があるため、re.findallの返り値に含まれてしまいます。
キャプチャを無効化するために(?:)としています9

普通の丸括弧の、キャプチャしない版です。

In [263]: re.findall(r"\w+(?:[-']\w+)*|'|[-.(]+|\S\w*", raw)
Out[263]:
["'",
 'When',
 "I'M",
 'a',
 'Duchess',
 ',',
 "'",
 'she',
 'said',
 'to',
 'herself',
 ',',
 '(',
 'not',
 'in',
 'a',
 'very',
 'hopeful',
 'tone',
 'though',
 ')',
 ',',
 "'",
 'I',
 "won't",
 'have',
 'any',
 'pepper',
 'in',
 'my',
 'kitchen',
 'AT',
 'ALL',
 '.',
 'Soup',
 'does',
 'very',
 'well',
 'without',
 '--',
 'Maybe',
 "it's",
 'always',
 'pepper',
 'that',
 'makes',
 'people',
 'hot-tempered',
 ',',
 "'",
 '...']

NLTK+正規表現を使ったトークン化

nltk.tokenize.regexp.regexp_tokenizeメソッドを使います10

In [253]: text = 'That U.S.A. poster-print costs $12.40...'

In [258]: pattern = r'''(?x)
     ...:     (?:[A-Z]\.)+  # U.S.A
     ...:   | \w+(?:-\w+)*  # That, poster-print
     ...:   | \$?\d+(?:\.\d+)?%?  # $12.40
     ...:   | \.\.\.  # ...
     ...:   | [][.,;"'?():-_`]  # separate tokens
     ...: '''

In [259]: nltk.regexp_tokenize(text, pattern)
Out[259]: ['That', 'U.S.A.', 'poster-print', 'costs', '$12.40', '...']

正規表現中の(?x)は「インラインフラグ」だそうで、他にaやiなどを取れるそうです11
インラインフラグ(?x)re.Xに相当し、正規表現の中でコメントが書けるようになります。
ただし、副作用としてr' 'として空白文字に一致させることができなくなるそうです。

トークン化については、正規表現を使ったやり方を学びました。

まとめ

自然言語処理(英文テキスト)の前処理の中から以下の2点を学びました。

  • ステミング:語幹を取り出す(=接辞を除く)
    • NLTKの組み込みのステマー(PorterStemmer
    • レマタイザ(WordNetLemmatizer
  • トークン化
    • 正規表現を使う(nltk.tokenize.regexp.regexp_tokenize

これは『入門 自然言語処理』3章 生テキストの処理のうち、3.1〜3.7を写経して学んだことのアウトプットです。

感想

『入門 自然言語処理』はボリューミーで正規表現、NLTKとお腹いっぱいです。
アウトプットしていませんが、Web上のテキストの取得やUnicodeの話などもありました(機会があれば書きたいです)。
テキストの取得〜前処理部分をまとめてインプットをする中で、これまでの経験がつながる感覚もありました。
NLTKは巨大なパッケージで、今回触ったところの他にも色々と寄り道素振りしがいがありそうです。

3章も少し残っていますが、来週は別の章に取り組む予定です。


  1. Python2系は全然触る機会がなかったのですが、sunsetした後にこんな形で2系と思い出作りすることになるとは思いませんでした

  2. リンク先を呼んでいただければおわかりいただけると思いますが、すばらしすぎる本なので禁書にすべきだそうです。バズり戦略ですね

  3. 詳しくは、エラーメッセージで案内されるリンク Installing NLTK Data — NLTK 3.4.5 documentationdownloader Moduleのドキュメント を見るとよさそうです。

  4. ref: http://www.nltk.org/images/pipeline1.png

  5. help(Text)で確認したところ、イニシャライザの引数tokensにはstrのシーケンスを渡すので、トークンのリストも渡せます

  6. ref: https://docs.python.org/ja/3/library/re.html#regular-expression-syntax

  7. ステミング処理を正規表現で実装したことは、正規表現での実現方法を知り、正規表現の素振りをするという「車輪の再実装」効果がありました。

  8. NLTKのドキュメントから Porter Stemming Algorithm が案内されています

  9. https://docs.python.org/ja/3/library/re.html#regular-expression-syntax

  10. 書籍中のコードはキャプチャ無効化がないことにより、想定通り動かないというバグがありました。オンライン版のコードを参考にしています

  11. 詳しくは(?aiLmsux)の項目を参照:https://docs.python.org/ja/3/library/re.html#regular-expression-syntax

TensorFlowのドキュメントを確認し、tf.py_functionが何をやっているのか理解を深めました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。

先週、総括としてライブドアニュース(日本語テキスト)の分類に取り組んだところ、tf.py_function何をやっているのか分からないという課題が見えました。
そこで、今回はドキュメントを参考にtf.py_functionの理解を深めました。
先週から参照している記事「tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる - Qiita 」のコードの書き換えにも挑戦しています。

先週の週1ブログ駆動開発!

目次

動作環境

先週と同じ環境です。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ # 注:コードを動かすのにGPUは使っていません
$ python -V
Python 3.7.3
$ pip list  # grepを使って抜粋して表示
tensorflow               2.1.0
tensorflow-datasets      2.0.0
matplotlib               3.1.2
numpy                    1.18.1
Janome                   0.3.10

tf.py_functionとは何かの前に:tf.data.Dataset.mapとは何か

ドキュメントはこちら

必須引数map_funcに焦点を当てます1

引数map_funcは関数(オブジェクト)を受け取り、

Maps map_func across the elements of this dataset.
(データセットの要素全体に map_func を適用する)

mapメソッドがやることは以下の2つです:

  • returns a new dataset((map_funcが適用された) 新しいtf.data.Dataset(以下では「データセット」と表記)オブジェクトを返す)
  • same order as they appeared in the input(要素は入力に現れたのと同じ順番)

map_funcの入力

The input signature of map_func is determined by the structure of each element in this dataset.
map_funcの入力シグネチャは現データセットオブジェクトの各要素の構造によって決定される)

シグネチャ(signature)は馴染みがない言葉なのですが、ここでは「パラメーターとその型」と理解しました。2

ドキュメントにはmap_funcの入力を決める例が3つ続きます:

  1. tf.Tensor1つから構成されたデータセットオブジェクト3
  2. 2つのtf.Tensorからなるタプル(tf.Tensor, tf.Tensor)から構成されたデータセットオブジェクト
  3. 辞書から構成されたデータセットオブジェクト

1の例(dataset.map(lambda x: x + 1))ではxtf.Tensor1つに対応します。

In [2]: dataset = tf.data.Dataset.range(1, 6)

In [7]: list(dataset.as_numpy_iterator())
Out[7]: [1, 2, 3, 4, 5]

In [8]: dataset2 = dataset.map(lambda x: x + 1)

In [9]: list(dataset2.as_numpy_iterator())
Out[9]: [2, 3, 4, 5, 6]

今回知ったデータセットオブジェクトの便利なメソッド

(1) 上の例でもlist()と合わせて使いましたが、as_numpy_iteratorメソッドが便利です。
for inでデータセットオブジェクトを扱わなくても値を見ることができます。

(2)from_generatorメソッドでデータセットオブジェクトを作る方法を学びました。

In [22]: elements = [(1, "foo"), (2, "bar"), (3, "baz")]

In [23]: dataset = tf.data.Dataset.from_generator(lambda: elements, (tf.int32, tf.string))

from_generatorメソッドの

  • generator引数にlambda(引数なし)を渡し、
  • output_types引数で型を定義

しています。

今回使った引数なしのlambda(無名関数)の確認

In [32]: f = lambda: elements

In [33]: f()
Out[33]: [(1, 'foo'), (2, 'bar'), (3, 'baz')]  # elememntsが返る

なお、「dataset2 = tf.data.Dataset.from_tensor_slices(elements)でもいいのでは」と試したところ、

ValueError: Can't convert Python sequence with mixed types to Tensor.

(型が混在したシーケンスはtf.Tensorに変換できない)とのことです。
だからfrom_generatorメソッドがあるのですね。納得!

map_funcの出力

入力を見たので続いて出力を見ます。

The value or values returned by map_func determine the structure of each element in the returned dataset.
map_funcの返り値が、mapで返されるデータセットオブジェクトの各要素の型を決める)

ドキュメントには、タプルを返すいくつかの例が紹介されていました。

  1. 関数gの例:要素が全て定数で変換されたdatasetが返される
  2. 関数hの例:リストとnumpy arrayはともにtf.Tensorに変換される
  3. 関数iはネストされたタプルの例

gの例です。

In [36]: def g(x):
    ...:     return tf.constant(37.0), tf.constant(["Foo", "Bar", "Baz"])
    ...:

In [37]: g(1)  # 実引数の値によらず返り値は同じ
Out[37]:
(<tf.Tensor: shape=(), dtype=float32, numpy=37.0>,
 <tf.Tensor: shape=(3,), dtype=string, numpy=array([b'Foo', b'Bar', b'Baz'], dtype=object)>)

In [38]: type(g(1))
Out[38]: tuple

In [39]: result = dataset.map(g)

In [40]: for d in result:  # 値だけの確認であれば list(result.as_numpy_iterator()) が使える
    ...:     print(d)
    ...:
(<tf.Tensor: shape=(), dtype=float32, numpy=37.0>, <tf.Tensor: shape=(3,), dtype=string, numpy=array([b'Foo', b'Bar', b'Baz'], dtype=object)>)
(<tf.Tensor: shape=(), dtype=float32, numpy=37.0>, <tf.Tensor: shape=(3,), dtype=string, numpy=array([b'Foo', b'Bar', b'Baz'], dtype=object)>)
(<tf.Tensor: shape=(), dtype=float32, numpy=37.0>, <tf.Tensor: shape=(3,), dtype=string, numpy=array([b'Foo', b'Bar', b'Baz'], dtype=object)>)

In [41]: result.element_spec
Out[41]:
(TensorSpec(shape=(), dtype=tf.float32, name=None),
 TensorSpec(shape=(3,), dtype=tf.string, name=None))

tf.Tensor2つからなるタプルがデータセットオブジェクトの1つ1つとなっていることが確認できます(40のセル参照)。

セル41で使っているelement_spec属性は便利そうです。

本題:tf.py_functionとは何か

mapのドキュメントを読んでいくと、map_funcPythonのコードを渡す方法について言及されています4

To use Python code inside of the function you have two options

  1. Rely on AutoGraph to convert Python code into an equivalent graph computation(AutoGraphを用いてPythonコードを等価なグラフ計算に変換する)
    • AutoGraphですべてのPythonコードを変換できるとは限らない
  2. Use tf.py_function
    • 任意のPythonコードを書ける
    • 1に比べて一般にパフォーマンスは劣る

mapのドキュメントからリンクされているtf.py_functionを見ていきます。

引数について

  • funcPythonの関数
  • inptf.Tensorのリスト
    • funcinpと型が対応するtf.Tensorのリストを受け付ける
  • Tout:TensorFlowのデータ型のリストorタプル。funcの返り値を示す(Noneが返るときは空のリストとする)
    • funcToutの値に対応する型のtf.Tensorのリストを返す

返り値は

A list of Tensor or a single Tensor which func computes
funcが計算するtf.Tensorのリストまたはtf.Tensor単体)

tf.py_functionの使い方を試す

mapのドキュメントにあるupper_case_fnの例で確認します(書き換えたいQiita記事との読み替えが減るように、関数名の先頭をアンダースコアで始めています)。

In [75]: d = tf.data.Dataset.from_tensor_slices(['hello', 'world'])

In [76]: def _upper_case_fn(t):
    ...:     return t.numpy().decode().upper()
    ...:

In [77]: d = d.map(lambda x: tf.py_function(func=_upper_case_fn, inp=[x], Tout=tf.string))

In [78]: list(d.as_numpy_iterator())
Out[78]: [b'HELLO', b'WORLD']

mapmap_funcに渡す関数オブジェクトをlambda以外で作ってみます。
lambdaを使っていないだけで、関数の処理内容は変わりません(tf.py_functionの適用結果を返す)。

In [79]: def upper_case_func(x):
    ...:     return tf.py_function(func=_upper_case_fn, inp=[x], Tout=tf.string)
    ...:

In [80]: d = tf.data.Dataset.from_tensor_slices(['hello', 'world'])

In [81]: list(d.map(upper_case_func).as_numpy_iterator())
Out[81]: [b'HELLO', b'WORLD']

というわけで、mapメソッドのmap_func引数には

  • tf.py_functionを返すlambda
  • または、tf.py_functionを返す関数

のどちらかを渡せばよさそうです。

自然言語処理tf.py_functionを使ったコードの書き換えに挑む

tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる - Qiita 中のコードを書き換えてみます。
例えば、janomeを使って原形を取り出す部分は、関数内関数を多用しているために書き手以外にわかりにくいコードという印象です。
もっとわかりやすく書けないか、自分の理解を深めるために書き直します。

書き換え前のコード

def janome_tokenizer():
    token_filters = [
        CompoundNounFilter(),
        POSStopFilter(["記号", "助詞", "助動詞", "名詞,非自立", "名詞,代名詞"]),
        LowerCaseFilter(),
        ExtractAttributeFilter("surface"),
    ]
    tokenizer = Tokenizer()
    analyzer = Analyzer(tokenizer=tokenizer, token_filters=token_filters)
    analyze_function = analyzer.analyze

    def _tokenizer(text_tensor, label):
        text_str = text_tensor.numpy().decode("utf-8")
        tokenized_text = " ".join(list(analyze_function(text_str)))
        return tokenized_text, label

    return _tokenizer


def tokenize_map_fn(tokenizer):
    def _tokenize_map_fn(text_tensor, label):
        return tf.py_function(
            tokenizer, inp=[text_tensor, label], Tout=(tf.string, tf.int64)
        )

    return _tokenize_map_fn


datasets = datasets.map(tokenize_map_fn(janome_tokenizer()))
  • tokenize_map_fn(janome_tokenizer())mapの実引数)で_tokenize_map_fn関数が返る
  • mapで適用する_tokenize_map_fnの引数text_tensor, labelは、データセットの各要素と一致
    • mapで渡す関数はデータセットの構成要素に対応する数の引数を受け取れる
    • 1つの関数でまとめようとすると、引数はtext_tensor, labelとする必要がある(tf.py_functioninp引数への指定で使う)

1つの関数にまとめてみた

def tokenize_map_fn():
    token_filters = [
        CompoundNounFilter(),
        POSStopFilter(["記号", "助詞", "助動詞", "名詞,非自立", "名詞,代名詞"]),
        LowerCaseFilter(),
        ExtractAttributeFilter("surface"),
    ]
    tokenizer = Tokenizer()
    analyzer = Analyzer(tokenizer=tokenizer, token_filters=token_filters)
    analyze_function = analyzer.analyze

    def _tokenizer(text_tensor, label):
        text_str = text_tensor.numpy().decode("utf-8")
        tokenized_text = " ".join(list(analyze_function(text_str)))
        return tokenized_text, label

    def _tokenize_map_fn(text_tensor, label):
        return tf.py_function(
            _tokenizer, inp=[text_tensor, label], Tout=(tf.string, tf.int64),
        )

    return _tokenize_map_fn


all_tokenized_data = all_labeled_data.map(tokenize_map_fn())
  • mapに渡るのはtokenize_map_fn()、すなわち_tokenize_map_fnという関数オブジェクト
    • _tokenize_map_fnはデータセットの要素を引数に受け取れる(def _tokenize_map_fn(text_tensor, label):
    • _tokenize_map_fntf.py_functionを適用したオブジェクトを返す
  • 関数tokenize_map_fnjanomeのAnalyzerを作った後、それを使った関数_tokenizerを用意し、_tokenize_map_fn関数の中に設定している

動作するがWarningが出る例

def tokenize_map_fn(text_tensor, label):
    token_filters = [
        CompoundNounFilter(),
        POSStopFilter(["記号", "助詞", "助動詞", "名詞,非自立", "名詞,代名詞"]),
        LowerCaseFilter(),
        ExtractAttributeFilter("surface"),
    ]
    tokenizer = Tokenizer()
    analyzer = Analyzer(tokenizer=tokenizer, token_filters=token_filters)
    analyze_function = analyzer.analyze

    def _tokenizer(text_tensor, label):
        text_str = text_tensor.numpy().decode("utf-8")
        tokenized_text = " ".join(list(analyze_function(text_str)))
        return tokenized_text, label

    return tf.py_function(
        _tokenizer, inp=[text_tensor, label], Tout=(tf.string, tf.int64),
    )


all_tokenized_data = all_labeled_data.map(tokenize_map_fn)

mapに直接渡せるようにtokenize_map_fntext_tensor, labelを引数で渡せるようにしました。
ですが、以下のようなWarningが大量に出ます。

WARNING:tensorflow:AutoGraph could not transform <bound method Tokenizer.tokenize of <janome.tokenizer.Tokenizer object at 0x13e673860>> and will run it as-is.
WARNING:tensorflow:Entity <bound method CompoundNounFilter.apply of <janome.tokenfilter.CompoundNounFilter object at 0x13e6732b0>> appears to be a generator function. It will not be converted by AutoGraph.

おそらくPythonのコードが全て、tf.py_functionで包まれていないのが原因ではないかと見ています。

感想

maptf.py_functionのドキュメントからだいぶ理解は深まりました。
ですが、実務レベルのコードでの適用方法は、書き換えでwarningに出会ったこともあり、まだ自信がありません(時間切れ感もあります)。

先週感じた課題の中からTensorFlowを今後使っていく上で影響しそうなtf.py_functionを選択しました。
まだまだ課題は残っていますし、新しい調査事項も出てきました。
Qiita記事のencodeの部分、私のコードではset_shapeが見られ、これを削除するとエラーになります。
これが何をするものなのか気になっています。

課題は山積していますが、いったんそれらは脇に置いて(時間が余った時にやるように優先度を下げて)、次週からは自然言語処理の基礎を固めるために、「禁書にすべき」という声もあるあの本に取り組みます。


  1. 今回はtf.py_functionがよく分からない問題に取り組んでおり、tf.py_functionが関わってくるのがmap_funcのためです。

  2. Signature (functions) (シグネチャ (関数)) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

  3. ドキュメントの上部「Source Datasets」の記載から、データセットオブジェクトをfor inで取り出したときの1つ1つがtf.Tensorと理解しています

  4. ここまでの例で単純なlambdaは渡せることが確認できたので、グラフとして扱う(tf.data traces the function and executes it as a graph)中でPythonのコードを使う方法のようです

tf.data でライブドアニュース(日本語テキスト)の分類に取り組んだところ、自分のコードの課題が見えました

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
2019年12月末から自然言語処理のネタで毎週1本ブログを書いています。
今週はここまでの総決算として、日本語テキストを分類するタスクにTensorFlowを使って取り組みました。

目次

動作環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ # 注:コードを動かすのにGPUは使っていません
$ python -V
Python 3.7.3
$ pip list  # grepを使って抜粋して表示
tensorflow               2.1.0
tensorflow-datasets      2.0.0
matplotlib               3.1.2
numpy                    1.18.1
Janome                   0.3.10

tensorflow-datasetsのバージョンが2系に上がっていました。

TensorFlowを使ったテキストの扱い

方法はいくつかあると思いますが、前回のブログ(英語テキストの場合)にならって tf.data を使う方法で進めます。

  1. テキストのダウンロード
  2. テキストを tf.data.Dataset に読み込む
  3. テキスト中の単語を数字にエンコードする
  4. データセットを、訓練用、バリデーション用、テスト用のバッチに分割する
  5. モデルの構築・訓練

日本語テキストで必要なステップ:形態素解析

日本語テキストと英語テキストの扱いの違いは形態素解析です。
英語は単語が半角スペースで区切られています(例:This is a pen)が、日本語は一続き(例:これはペンです)です。
単語の区切りを見つける必要があり、その方法の1つが形態素解析です。
形態素解析で単語の区切りを見つけることで、英語のように半角スペース区切りで単語を並べることもできます(これ は ペン です)

形態素解析は2と3の間に必要です。
今回は取り組みの中で触ったことのあるjanomeを使用しました。

今週のネタ探しで参照した記事1janomeを使ったコード例があったのも採用理由です。

1. テキストのダウンロード:ライブドアニュースのデータセット

コードでダウンロードするのではなく、手動でダウンロードします。
https://www.rondhuit.com/download.html からダウンロードした「livedoor ニュースコーパス2を使いました。3

展開方法はこちら。
カレントディレクトリにtextディレクトリができます

$ tar xzf ~/Downloads/ldcc-20140209.tar.gz
$ tree -L 1 text/ 
text/ 
├── CHANGES.txt 
├── README.txt 
├── dokujo-tsushin 
├── it-life-hack 
├── kaden-channel 
├── livedoor-homme 
├── movie-enter 
├── peachy 
├── smax 
├── sports-watch 
└── topic-news

tar --helpを見たところ、fオプションによりアーカイブを指定すればいいことに気づきました(以下に抜粋)。

First option must be a mode specifier:
  -c Create  -r Add/Replace  -t List  -u Update  -x Extract
Common Options:
  -f <filename>  Location of archive
  -z, -j, -J, --lzma  Compress archive with gzip/bzip2/xz/lzma

2. テキストを tf.data.Dataset に読み込む

def labeler(example, index):
    return example, tf.cast(index, tf.int64)


text_datasets = []
label_count = 0

text_dir = os.path.join(os.getcwd(), "text")
text_subdir_names = tf.compat.v1.gfile.ListDirectory(text_dir)
for subdir in text_subdir_names:
    data_dir = os.path.join(text_dir, subdir)
    if os.path.isdir(data_dir):
        print(f"{label_count}: {subdir}")
        text_file_names = tf.compat.v1.gfile.ListDirectory(data_dir)

        text_tensors = []
        for file_name in text_file_names:
            text_file = os.path.join(data_dir, file_name)
            lines_dataset = tf.data.TextLineDataset(text_file)
            # 1行1行がTensorとなるので、ファイルの文章全体をつないでTensorとする
            sentences = [
                line_tensor.numpy().decode("utf-8")
                for line_tensor in lines_dataset
            ]
            concatenated_sentences = " ".join(sentences)
            # subdirのファイルごとにTensorを作り、Datasetとする
            text_tensor = tf.convert_to_tensor(concatenated_sentences)
            text_tensors.append(text_tensor)
        text_dataset = tf.data.Dataset.from_tensor_slices(text_tensors)
        text_dataset = text_dataset.map(lambda ex: labeler(ex, label_count))
        text_datasets.append(text_dataset)
        label_count += 1

textディレクトリ以下のサブディレクトリを取得4し、その中のファイルの一覧も取得、1つ1つのファイルについてTextLineDatasetで読み込みます。
今回の場合、サブディレクトリはクラスを表し、その中に複数のテキストファイルがあります。

ここで苦労したのは、TextLineDatasetは1行ずつ読み込まれるという挙動5

  • やりたいこと:テキストファイル全体に含まれる複数行のテキストを最小単位としてtf.data.Datasetに読み込む
  • TextLineDatasetでは、テキストファイルの1行ごとに分かれて読み込まれる

ひねり出した解決策

  1. 1ファイルをTextLineDatasetとして読み込んだら、すべての行を一度取り出し、つないだtf.Tensor(以下テンソル)とする(1ファイルに対応するテンソル):tf.convert_to_tensor
  2. 1のテンソルをリストに入れる(クラスごとのファイルに対応するテンソルが1つのリストに入る。リストはクラスを表す)
  3. 2のリストからtf.data.Datasetを作る:tf.data.Dataset.from_tensor_slices6

これで前回のブログのホメロスのケースと同じ扱いになったので、concatenateで1つのDatasetにまとめ、shuffleします

all_labeled_data = text_datasets[0]
for labeled_data in text_datasets[1:]:
    all_labeled_data = all_labeled_data.concatenate(labeled_data)

all_labeled_data = all_labeled_data.shuffle(
    BUFFER_SIZE, seed=RANDOM_SEED, reshuffle_each_iteration=False
)

追加:Dataset中のテキストを形態素解析する

Analyzerはこれまでで学んだことが活きました。
tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる - Qiita (☆)にならって、関数_tokenizerを返します

def janome_tokenizer():
    token_filters = [
        CompoundNounFilter(),
        POSStopFilter(["記号", "助詞", "助動詞", "名詞,非自立", "名詞,代名詞"]),
        LowerCaseFilter(),
        ExtractAttributeFilter("surface"),
    ]
    tokenizer = Tokenizer()
    analyzer = Analyzer(tokenizer=tokenizer, token_filters=token_filters)
    analyze_function = analyzer.analyze

    def _tokenizer(text_tensor, label):
        text_str = text_tensor.numpy().decode("utf-8")
        tokenized_text = " ".join(list(analyze_function(text_str)))
        return tokenized_text, label

    return _tokenizer

mapで適用します。

all_tokenized_data = all_labeled_data.map(tokenize_map_fn(janome_tokenizer()))

3. テキスト中の単語を数字にエンコードする

前回の記事と同じです。

tokenizer = tfds.features.text.Tokenizer()
vocabulary_set = set()
for text_tensor, _ in tqdm(all_tokenized_data):
    tokens = tokenizer.tokenize(text_tensor.numpy().decode("utf-8"))
    vocabulary_set.update(tokens)
vocab_size = len(vocabulary_set)
print(f"vocabulary size: {vocab_size}")

encoder = tfds.features.text.TokenTextEncoder(vocabulary_set)
all_encoded_data = all_tokenized_data.map(encode_map_fn)

4. データセットを、訓練用、バリデーション用、テスト用のバッチに分割する

これも前回の記事のとおりです。
(☆)にならって関数化してもいいかも

output_shapes = tf.compat.v1.data.get_output_shapes(all_encoded_data)
test_data = all_encoded_data.take(TAKE_SIZE)
test_data = test_data.padded_batch(BATCH_SIZE, output_shapes)
train_data = all_encoded_data.skip(TAKE_SIZE).shuffle(
    BUFFER_SIZE, seed=RANDOM_SEED
)

val_data = train_data.take(TAKE_SIZE)
val_data = val_data.padded_batch(BATCH_SIZE, output_shapes)
train_data = train_data.skip(TAKE_SIZE).shuffle(BUFFER_SIZE, seed=RANDOM_SEED)

train_data = train_data.padded_batch(BATCH_SIZE, output_shapes)
vocab_size += 1  # padded_batchした際に0という番号を追加している

vocab_size += 1を忘れたところ、学習中にインデックス範囲外のインデックス参照が発生して、落ちました。

5. モデルの構築・訓練

前回の記事で知ったモデルを使います。

model = tf.keras.Sequential(
    [
        tf.keras.layers.Embedding(vocab_size, 64),
        tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),
        tf.keras.layers.Dense(64, activation="relu"),
        tf.keras.layers.Dense(64, activation="relu"),
        tf.keras.layers.Dense(9, activation="softmax"),
    ]
)

model.compile(
    optimizer="adam",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

コードの書き方が悪くて耐えがたい遅さ

Datasetはステップ3のfor文の箇所で初めて評価されるようです(遅延評価という理解)。
ここの処理が遅く、扱うテキスト量の問題と考え、分類に使うテキストを減らしていきました。
最終的にit-life-hackとdokujo-tsushinの2クラス分類になりました。
テキスト数の概要としては1700件程度です(これでも件のfor文に2分半ほどかかります)。

$ ls -l text/dokujo-tsushin/ | wc -l
     872
$ ls -l text/it-life-hack/ | wc -l
     872

訓練用:バリデーション用:テスト用の分割は8:1:1としています。

遅さはどうすれば解消するか

今回のコードには問題があるので、全クラスを対象にできるように今後コードを書き換えます。
今の段階で考えているのは以下のような案です:

  • (☆)の記事のようにtf.py_functionを活用して実行する回数を減らす
    • tf.py_functionが何をやっているのかがいま一つわからず、まだ使いこなせていない課題
    • データの分割でshuffleの回数が減らせそう
  • 形態素解析器をjanomeからmecabに変えてみる
    • Analyzerを都度作っていて遅いというわけではなさそう(関数のローカル変数からグローバル変数に移して比較した)
    • ラッパーは今だとfugashiを使うといいらしい7
  • 1つのスクリプトで全部やろうとせずに中間ファイルを作ってはどうだろう

モデルの評価:2クラス分類させたら

On test data: loss=0.07063097149754564, acc=0.9733333587646484

ITの記事と女性向けの記事は違いが顕著なようで、また、MLPと比べたら多少はいいモデルを使っているので、実運用できそうなモデルができあがりました。
問題は実行時間がだいぶかかる(毎回コーヒー飲める)ことですね。。

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

感想

動くようにしたコードはGitHubに上げました:

今週は木曜の登壇日曜のリジェクトコン開催で、この取り組みの時間を作れずめちゃくちゃ厳しい週でした。
今回のスクリプトの実行に10分程度かかり、タイムリミットは迫り、「機械学習の取り組みは時間に追われてやるものではないな」と心底感じました。

見るコードによってbytes.decode()の引数がまちまちだなと思ったのですが、これは第1引数encodingのデフォルトが'utf-8'だから書かなくてもいいということでした。8

この取り組みをそろそろモデルの方にシフトしこうと思っていたのですが、今週残した宿題は結構大きい認識なので、もう少しだけ続きに取り組みます(来週の目標は宿題を解消してBERTに触る!)。9


  1. tf.data.Dataset apiでテキスト (自然言語処理) の前処理をする方法をまとめる - Qiita

  2. データセットの説明は今回参照した Mecabとtf.dataを使ってlivedoorニュースコーパスを分かち書きする - Qiita にあります

  3. これを使って過去にこんなこともしています:イベントレポート | #pyhack にて旦那と彼氏の間に何があるのかをword2vecに聞いてみました - nikkie-ftnextの日記

  4. 使ったのがtf.compat.v1.gfile.ListDirectory。これはpathlib.Pathを受け付けなくてがっかりでした

  5. 前回のホメロスの翻訳の分類のように、クラスごとにテキストが1ファイルで1つ1つの文をクラスに分類する際に重宝すると思います。他にはポジティブな感情を表すテキストファイルとネガティブな感情を表すテキストファイルの場合が考えられます

  6. 似たメソッドの tf.data.Dataset.from_tensors と結果を比較して選択しました

  7. ref: Mecabとtf.dataを使ってlivedoorニュースコーパスを分かち書きする - Qiita

  8. ref: https://docs.python.org/ja/3/library/stdtypes.html#bytes.decode

  9. リジェクトコンで知ったnagisaや最近耳にするGiNZAと前処理の新しい潮流も触っておきたいなという思いも出てきています。悩ましいです

告知 | 今週末 1/26 (日) は 「熱意あるPython使いの発表会」です(申込みは1/21(火)いっぱいまで!) #rejectpy

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
PyCon JP 2019 リジェクトコンとして企画した勉強会の最後の告知です。

前回のリジェクトコンエントリ!

勉強会の企画内容

※すでに申し込まれている方、connpassメッセージで送っている入館申請のフォームにご回答ください。

期日

1/26(日)に開催します。
入館申請の都合上、申し込みは1/21(火)いっぱいとします。
迷っている方はぜひお申し込みください!

コンテンツ

後述しますが、リジェクトされたプロポーザルをお持ちの発表者集めは苦戦しました。
そんな中、企画した勉強会に参加申し込みいただけたことがとてもありがたかったので、

  • 発表者の枠をリジェクトされたプロポーザルを持っている方以外にPython関係で発表したい方」へと広げ
  • 参加者の方に実りある時間にしようとアイディア出しの結果、余った時間はYouTubePyCon JP 2019アーカイブを見る(アンケートで評価が高いトークを中心に)

としています。

(これを読んでいるあなた、Python関係の発表ネタがあればドタ参どうぞ。@ftnextまでご連絡ください)

「名は体を表す」というように、「熱意あるPython使いの発表会」となっていると思います。

会場設備

ふだん勉強会に参加して当たり前のように使っているWi-Fi、プロジェクター、電源などですが、運営する側になるとチェックが必要ですね。
今回早稲田大学さんから借りる会議室には、Wi-Fi、プロジェクター、電源が揃っているとのことです(多謝)。
設備リストはチェックリスト化してあると慌てなくて済むと思いました。

企画〜発表者募集して感じた難しさ

「半年近くが経過してのリジェクトコンは難しい」この一言に尽きます(まだ開催してすらいないですけれどね)。

まずpapercallでプロポーザルを公開している方が思っていたより少なかったです(検索の仕方が悪かったのか、数名しか見つかりませんでした)。
発表者が集まらない状況が続く中、発表者がいないと成立しないので、数名お声がけもしたのですが、日程の都合がつかず参加は難しいとのことでした。残念。。

準備中に読んだ『ワンストップ勉強会』に「ツテを頼る」と書いてあって膝を打ったのですが、

勉強会は登壇者をある程度押さえてから開催告知をする

というのが今回の教訓です。

最後に

運営を手伝っていただいている方、発表していただける方、そして参加表明してくださった方に感謝です。
当日まで走り抜けたいと思います。
いよいよ今週末です、楽しみましょう!

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