Reactチュートリアル入門: Pythonサーバーサイド連携編

チュートリアル | React がなくなったようで。。良いチュートリアルだったのに。。
まあとりあえず、前回は クライアントサイドのみでWebサイトを表示したので、 今回はサーバーサイドで取得したデータをクライアントサイドに渡して表示させる。 とは言っても、JSONファイルを表示するだけなので、非常に簡単。 ただし、サーバーサイドはNode.jsではなく、Pythonを使う。 その前にまず、前回のReactコードを少しカスタマイズする。

クライアントサイドのカスタマイズ

CommentBox

前回、シンプルだったCommentBoxに色々追記する。

  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },

Ajax通信を行う。urlは、後ほどReactDOM.renderでCommentBoxに渡される。 これがサーバーサイドへのパスになる。dataはサーバーサイドから返ってきたデータ。  

  getInitialState: function() {
    return {data: []};
  },

クライアントサイドのdataの初期化。dataはObjectのArrayなので[]となる。  

  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },

DOMに関わる初期化。 loadCommentsFromServerで設定したAjaxリクエストや、リフレッシュ頻度を指定するsetIntervalの登録など、 server-side rendering時には必要ない初期化処理についてはこの中で行う。

以上より、今回のCommentBoxは次のようになる。  

var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList comments={this.state.data} />
      </div>
    );
  }
});

ReactDOM.render

設定したCommentBoxに、ReactDOM.renderでパラメーターを渡す。 サーバーサイドのパスとなるurlと、リフレッシュ頻度となるpollIntervalを指定する。 前回はurlキーではなく、dataキーにクライアントサイドで定義したdataを渡していた。 pollIntervalは公式では2000(2秒)だが、短すぎるので2000秒としておく。

ReactDOM.render(
  <CommentBox url="/api/comments" pollInterval={2000000} />,
  document.getElementById('content')
);

Pythonサーバーサイド

Pythonのサーバーサイドは、Flaskを使っている。 Flaskは僕も知らないので、ググって下さい。 でもFlaskはまったく悩むことなく簡単に使えるようで、 クライアントサイドで指定したurl='/api/comments' [check1!] のすぐ下のcomments_handler() の中でデータを取得して返している。 今回はGETのみなので、単純にserver.pyと同じディレクトリーに置かれたcommnet.jsonを読み込んで [check2!] 、 json形式にPythonで適切に変形して、クライアントサイドに返している [check3!] ことが分かる。

import json
import os
import time
from flask import Flask, Response, request

app = Flask(__name__, static_url_path='', static_folder='public')
app.add_url_rule('/', 'root', lambda: app.send_static_file('index.html'))  # client and server side with json file

@app.route('/api/comments', methods=['GET', 'POST'])  # Check1!
def comments_handler():
    with open('comments.json', 'r') as f:
        comments = json.loads(f.read())  # Check2!

    if request.method == 'POST':
        new_comment = request.form.to_dict()
        new_comment['id'] = int(time.time() * 1000)
        comments.append(new_comment)

        with open('comments.json', 'w') as f:
            f.write(json.dumps(comments, indent=4, separators=(',', ': ')))

    return Response(
        json.dumps(comments),  # Check3!
        mimetype='application/json',
        headers={
            'Cache-Control': 'no-cache',
            'Access-Control-Allow-Origin': '*'
        }
    )

if __name__ == '__main__':
    app.run(port=int(os.environ.get("PORT", 3000)), debug=True)

実行

$ python server.py

上記のコマンドを実行すると、以下のように表示されるはず。

f:id:Shoto:20161030002637p:plain

Reactチュートリアル入門:クライアントサイド編

チュートリアル | React(何故か2016-10-23時点ではNot Found) を理解しながら写経すると、Reactで簡単なWebアプリが作れるようになる。 しかし、僕自身もそうだが、テンプレートを使ったWebアプリしか作ったことしかなく、 Reactを理解したいという人向けに、今回はクライアントサイドだけをいじってWebアプリを作成する。 そもそもReactとは何か、Reactを使うと何が便利なのかを知りたい人は Reactを使うとなぜjQueryが要らなくなるのか が分りやすい。

Reactチュートリアルの始め方

Reactチュートリアルのプロジェクトをクローンして、パッケージをインストールする。

$ git clone https://github.com/reactjs/react-tutorial.git
$ cd react-tutorial
$ npm install

初期設定

プロジェクトの中身を見ると、色々ファイルが入っているので、必要なものだけに絞って残りは削除する。

react-tutorial
├── LICENSE
├── README.md
├── app.json
├── comments.json
├── db.py
├── db.pyc
├── node_modules
│   ├── body-parser
│   └── express
├── npm-debug.log
├── package.json
├── public
│   ├── css
│   ├── index.html
│   └── scripts
├── requirements.txt
├── server.go
├── server.js
├── server.php
├── server.pl
├── server.py
└── server.rb

 
以下が削除した結果。

react-tutorial
├── comments.json     // 今回は使わない
├── node_modules      // npm installで生成されたフォルダー
│   ├── body-parser
│   └── express
├── package.json      // メタ情報ファイル
├── public
│   ├── css
│   ├── index.html    // 今回編集する唯一のファイル
│   └── scripts
├── requirements.txt  // 後で使う
├── server.js         // 後で実行する
└── server.py         // 後で実行する
  • comments.json
    • コメントが書かれたJSONファイル。今回は使わない。
  • node_modules
    • "npm install"を実行した時に生成されたフォルダー。Reactなど今回使うnode.js関連のパッケージが入っている。基本触らない。
  • package.json
    • ライセンスや著者、バージョンなど記述されたメタ情報ファイル。"npm install"の後でパッケージの依存関係の記述も自動的に追加されている。
  • public
    • HTML・CSSJavaScript・画像など、クライアントサイドのファイルを置く場所。
  • index.html
    • 今回は編集する唯一のファイル。
  • server.jsとserver.py
    • アプリケーション・サーバーを起動するNode.jsとPythonのコード。他の言語を使いたい人は、それらのファイルを残す。
  • requirements.txt
    • Pythonのサーバーサイドを実行する際に必要なライブラリーをインストールするための設定ファイル。

index.htmlを開くと以下のようになるが、今回は下の方のscriptダグで囲まれたエリアにReactコードを追記していく。 そのため、"script/example.js"をインポートしているscriptタグをコメントアウトしておく。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>React Tutorial</title>
    <!-- Not present in the tutorial. Just for basic styling. -->
    <link rel="stylesheet" href="css/base.css" />
    <script src="https://unpkg.com/react@15.3.0/dist/react.js"></script>
    <script src="https://unpkg.com/react-dom@15.3.0/dist/react-dom.js"></script>
    <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
    <script src="https://unpkg.com/jquery@3.1.0/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/remarkable@1.7.1/dist/remarkable.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <!--
    <script type="text/babel" src="scripts/example.js"></script>
    -->
    <script type="text/babel">
    // To get started with this tutorial running your own code, simply remove
    // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

Reactの実装

最終的なWebアプリの見た目はこんな感じ。

f:id:Shoto:20161023190302p:plain

 
まずは、Webアプリで表示するためのJSONを記述する。

      var data = [
        {id: 1, author: "Pete Hunt", text: "This is one comment"},
        {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
      ];

 
ここからReact。 上記ので記述したdataを、CommentBoxに入れた結果を、index.htmlの<div id="content"></div>内にレンダリングする、 という意味のコードを記述する。 レンダリングするDOMの構造を、このCommentBoxから始まるReactによって作成することが、今回の主な内容であり、Reactのコアな部分になる。

      ReactDOM.render(
        <CommentBox data={data} />,
        document.getElementById('content')
      );

 
上記のdataキーは、CommentBoxでは{this.props.data}として表される。 逆に言えば、親のデータは{this.props.xxx}で取得できる。 また{this.props.data}は、子であるCommentListcommentsキーにそのまま渡される(オリジナルはdataキー)。 ここより下は、コメントのリストが表示されるので、h1タグでCommentsと記述しておく。

      var CommentBox = React.createClass({
        render: function() {
          return (
            <div className="commentBox">
              <h1>Comments</h1>
              <CommentList comments={this.props.data} />
            </div>
          );
        }
      });

 
上記のcommentsキーは、CommentListでは{this.props.comments}となる。 これはJSONのobjectのarrayなので、個々のobjectを子であるCommentに渡すために、 map関数を使っている。またCommentへはobjectの値をセットしている。

      var CommentList = React.createClass({
        render: function() {
          var commentNodes = this.props.comments.map(function (comment) {
            return (
              <Comment author={comment.author} key={comment.id}>
                {comment.text}
              </Comment>
            );
          });
          return (
            <div className="commentList">
              {commentNodes}
            </div>
          );
        }
      });

 
上記のauthorキーは{this.props.author}になる。 また上記の{comment.text}{this.props.children}になる。

      var Comment = React.createClass({
        render: function() {
          return (
            <div className="comment">
              <h2>
                {this.props.author}
              </h2>
              {this.props.children}
            </div>
          );
        }
      });

 
このようにReactによって、CommetBox以下でDOMの構造が構築された後、最終的にレンダリングされる。 index.htmlに追記したコードをまとめると以下のようになる。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>React Tutorial</title>
    <!-- Not present in the tutorial. Just for basic styling. -->
    <link rel="stylesheet" href="css/base.css" />
    <script src="https://unpkg.com/react@15.3.0/dist/react.js"></script>
    <script src="https://unpkg.com/react-dom@15.3.0/dist/react-dom.js"></script>
    <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
    <script src="https://unpkg.com/jquery@3.1.0/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/remarkable@1.7.1/dist/remarkable.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <!--
    <script type="text/babel" src="scripts/example.js"></script>
    -->
    <script type="text/babel">
      // To get started with this tutorial running your own code, simply remove
      // the script tag loading scripts/example.js and start writing code here.
      var data = [
        {id: 1, author: "Pete Hunt", text: "This is one comment"},
        {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
      ];

      var CommentBox = React.createClass({
        render: function() {
          return (
            <div className="commentBox">
              <h1>Comments</h1>
              <CommentList comments={this.props.data} />
            </div>
          );
        }
      });

      var CommentList = React.createClass({
        render: function() {
          var commentNodes = this.comments.data.map(function (comment) {
            return (
              <Comment author={comment.author} key={comment.id}>
                {comment.text}
              </Comment>
            );
          });
          return (
            <div className="commentList">
              {commentNodes}
            </div>
          );
        }
      });

      var Comment = React.createClass({
        render: function() {
          return (
            <div className="comment">
              <h2>
                {this.props.author}
              </h2>
              {this.props.children}
            </div>
          );
        }
      });

      ReactDOM.render(
        <CommentBox data={data} />,
        document.getElementById('content')
      );
    </script>
  </body>
</html>

アプリケーション・サーバーの実行

以下のいずれかを実行し、http://localhost:3000/にアクセスすれば、 2つのコメントがレンダリングされた画面が表示されるはず。

  • Node.js
node server.js
pip install -r requirements.txt
python server.py

pipの実行は初回だけでよい。

参考文献

Atomがフリーズしたときの対処法

macOS Sierraにアップデートしたら、"Editor is not responding"となったので、その対処法をメモに残しておく。

手順

  1. Finderのメニューバーから、[移動] > [フォルダーへ移動...]と選択
  2. テキストボックスに<~/.atom/storage>を入力して[移動]ボタンをクリック
  3. フォルダー内のファイルを削除

なお、Atom関連の設定が色々と飛ぶようなので、自己責任で。 僕の場合、ファイルは"application.json"しかなかった。 削除したら、"Editor is not responding"というメッセージが表示されることもなくなったし、 インストールしたパッケージやテーマも飛ばなかった。

参考文献

Python製プログレスバーを3行で書く

for文の中に標準出力を書くと、ターミナルが文字で埋め尽くされるので回避したいが、 for文の中身が重くて無反応になるのも困る。 なのでプログレスバーを書けば良い、という結論に至るわけだが、 さくっと色んなfor文に書きたいので、3行で書く方法を説明する。

インストール

progressbarというそのままな名前のライブラリーをインストールする。 2016年10月現在は、progressbar2 3.10.1という謎のバージョン。

# pip install progressbar2

実装

手順は次の3つ。

  1. インポート
  2. 初期化
  3. 更新

for文の外でインポートと初期化を行い、for文の中で更新を行う。 初期化の時に、繰り返し回数をパラメーターに指定する。 またfor文の中では、個々のデータではなく、全てのデータをindex(i)で指定して扱えるようにする。 なおサンプルとして、RSSを入れるとページのソースを取得してくれるメソッドget_src()を利用する。 実装は次の通り。

from progressbar import ProgressBar  # 1. インポート

def main(rss):
    pb = ProgressBar(maxval=len(rss)).start()  # 2. 初期化
    for i in range(len(rss)):
        get_src(rss[i])
        pb.update(i+1)  # 3. 更新

結果

ターミナルの幅をフルで使った出力になる。 カスタマイズしたい場合は、参考文献を参照。

f:id:Shoto:20161009165725g:plain

参考文献

Luhnの要約アルゴリズムの後処理

これまでLuhnの要約アルゴリズム前処理として基本的な自然言語処理を、 本処理としてテキストの各文のスコア付けを行った。 今回は後処理として要約テキストとして表示させる。 要約テキストは、スコア付けされた文を選択することで生成される。 ここでは2種類の選択方法について説明する。

各文のスコア

各文のスコアは以下のようになる。

index score sentence
0 5.1 スマートフォンでもPCでもオフラインでも使え、100以上の言語に対応しているGoogleの翻訳サービス「Google翻訳」が、ちょうど10周年を迎えるタイミングでより自然な翻訳を可能にする人工知能(AI)を活用した「GNMT」システムを発表しました
1 6.8 AI研究で複数の大手IT企業とのパートナーシップを結んだばかりのGoogleは、AIを駆使した翻訳システム「GNMT(Google Neural Machine Translation)」を発表しました
2 2.3 Google翻訳ではこれまでフレーズベースで機械翻訳するPBMTというシステムが採用されていましたが、このような単語やフレーズごとに機械的に文章を翻訳する方法ではなく、文章全体をひとつの翻訳単位として捉えることができるのがGNMTだそうです
3 2.3 Googleの研究者によると、「GNMTの『文章全体をひとつの翻訳単位として捉える』アプローチの長所は、工学的設計の選択肢がPBMTよりも少なくて済むことです」とのこと
4 1.3 実際にGNMTを駆使した最初の翻訳では、既存のGoogle翻訳と遜色ない翻訳精度がみられたそうです
5 1.8 さらに、何度も翻訳を重ねることで、GNMTは優れた翻訳と素早い翻訳スピードの両立が可能になっている模様
6 4.5 Googleによると、GNMTを用いることでGoogle翻訳は翻訳ミスを55~85%も軽減できるようになるとのこと
7 2.7 GoogleによるとGNMTは一部のケースでは人間レベルの翻訳が可能なレベルに達しているとのこと
8 3.2 以下のグラフは人間・GNMT・PBMTの3つによる翻訳を6段階評価して比較したもので、最も翻訳精度が高いのは人間による翻訳ですが、フランス語から英語に翻訳する場合や英語からスペイン語に翻訳する場合、人間とGNMTの間にそれほど大きな差は存在しないそうです

標準偏差による文の選択

0.5σ以上のスコアを持つ文を選択する。

# 要約アプローチ1
# 平均スコアとフィルタとしての標準偏差を使って重要ではない文を取り除く
avg = numpy.mean([s[1] for s in scored_sents])
std = numpy.std([s[1] for s in scored_sents])
mean_scored = [(sent_idx, score) \
               for (sent_idx, score) in scored_sents \
               if score > avg + 0.5 * std]

平均 3.3, 標準偏差1.7なので、0.5σは4.2となる。 そのため0, 1, 6番目の文が選択される。

>>> print mean_scored
[(0, 5.142857142857143), (1, 6.75), (6, 4.5)]

スマートフォンでもPCでもオフラインでも使え、100以上の言語に対応しているGoogleの翻訳サービス「Google翻訳」が、ちょうど10周年を迎えるタイミングでより自然な翻訳を可能にする人工知能(AI)を活用した「GNMT」システムを発表しました。AI研究で複数の大手IT企業とのパートナーシップを結んだばかりのGoogleは、AIを駆使した翻訳システム「GNMT(Google Neural Machine Translation)」を発表しました。Googleによると、GNMTを用いることでGoogle翻訳は翻訳ミスを55~85%も軽減できるようになるとのこと。

TopNの文の選択

単純に指定された数の文だけスコアが高い順に選択する。

self.TOP_SENTENCES = 5  # 「トップN」要約のために返す文の数

# 要約アプローチ2
# トップNにランクされた文だけを返す
top_n_scored = sorted(scored_sents, key=lambda s:s[1])[-self.TOP_SENTENCES:]
top_n_scored = sorted(top_n_scored, key=lambda s:s[0])

そのため、0, 1, 6, 7, 8番目の文が選択される。

>>> print top_n_scored
[(0, 5.142857142857143), (1, 6.75), (6, 4.5), (7, 2.6666666666666665), (8, 3.2)]

スマートフォンでもPCでもオフラインでも使え、100以上の言語に対応しているGoogleの翻訳サービス「Google翻訳」が、ちょうど10周年を迎えるタイミングでより自然な翻訳を可能にする人工知能(AI)を活用した「GNMT」システムを発表しました。AI研究で複数の大手IT企業とのパートナーシップを結んだばかりのGoogleは、AIを駆使した翻訳システム「GNMT(Google Neural Machine Translation)」を発表しました。Googleによると、GNMTを用いることでGoogle翻訳は翻訳ミスを55~85%も軽減できるようになるとのこと。GoogleによるとGNMTは一部のケースでは人間レベルの翻訳が可能なレベルに達しているとのこと。以下のグラフは人間・GNMT・PBMTの3つによる翻訳を6段階評価して比較したもので、最も翻訳精度が高いのは人間による翻訳ですが、フランス語から英語に翻訳する場合や英語からスペイン語に翻訳する場合、人間とGNMTの間にそれほど大きな差は存在しないそうです。

Luhnの要約アルゴリズムの本処理

Luhnの要約アルゴリズムの前処理の続き。 記事本文を"。"で分割して文のリストにし、さらに英語を小文字化したのがnormalized_sents(以下、sents)だった。 ここでは、分割された各文がどのようにスコア付けされるかを見ていく。

文を単語に分割する

今回は、以下の文の処理の流れを追っていく。

googleによるとgnmtは一部のケースでは人間レベルの翻訳が可能なレベルに達しているとのこと

nltkjp.word_tokenize()分かち書きを行うメソッド。

scores = []
sent_idx = -1
nltkjp = NLTKJP()
for s in [nltkjp.word_tokenize(sent) for sent in sents]:
    sent_idx += 1

文が単語に分割されたので、sの中身は次のようになる。

>>> for d in s:
>>>     print d,
>>> print
google に よる と gnmt は 一部 の ケース で は 人間 レベル の 翻訳 が 可能 な レベル に 達し て いる と の こと

文中の重要単語の位置を把握する

Luhnの要約アルゴリズムの前処理 で、頻出名詞Top100をimportant_wordsとして取得した。 これらが文中のどこに位置するかを把握する。

    word_idx = []

    # 単語リスト中の個々の単語について
    for w in important_words:
        try:
            # 文中の重要単語が出現した位置のインデックスを計算する
            word_idx.append(s.index(w))
        except ValueError, e:  # この文にはwが含まれていない
            pass

    word_idx.sort()

    # 一部の文は、重要単語を1つも含んでいないことがありえる
    if len(word_idx) == 0:
        continue

位置は次のようになる。

>>> print word_idx
[0, 4, 6, 8, 11, 12, 14, 16]

実際に単語に分割した文と重要単語の位置を比較すると、次のようになる。 上記のアルゴリズムでは文中に同じ重要単語が出てきた時にカウントできないことが分かる。 次の例では"レベル"が12と18にあるが、12しかカウントされていない。 意図的ではなくミスだと思うので、後で改修する。

文中の重要単語の距離からクラスタリングを行う

文中の重要単語間の距離が3以内であれば、それらを1つのクラスタとする。 オリジナルの距離は5。

    self.CLUSTER_THRESHOLD = 3  # 考慮する単語の間の距離

    # 単語のインデックスを使って2つの連続する単語に対して
    # 最大距離の閾値を使ってクラスタを計算する
    clusters = []
    cluster = [word_idx[0]]
    i = 1
    while i < len(word_idx):
        if word_idx[i] - word_idx[i-1] < self.CLUSTER_THRESHOLD:
            cluster.append(word_idx[i])
        else:
            clusters.append(cluster[:])
            cluster = [word_idx[i]]
        i += 1
    clusters.append(cluster)

実行すると3つのクラスタに分類された。

>>> print clusters
[[0], [4, 6, 8], [11, 12, 14, 16]]

クラスタのスコアを計算する

クラスタ内の重要単語数の二乗を、クラスタの最初と最後の距離で割った値を、そのクラスタのスコアとする。 最終的に各クラスタのスコアの最大値がその文のスコアになる。

    # 各クラスタのスコアを計算。クラスタのスコアの最大値がその文のスコア
    score_list = []
    max_cluster_score = 0
    for c in clusters:
        swc = len(c)  # significant_words_in_cluster
        twc = c[-1] - c[0] + 1  # total_words_in_cluster
        score = 1.0 * swc*swc / twc
        score_list.append(score)

        if score > max_cluster_score:
            max_cluster_score = score

    scores.append((sent_idx, max_cluster_score))

3つのクラスタとスコアとその最大値(文のスコア)は次のようになる。

>>> print score_list
[1.0, 1.8, 2.7]

>>> print max_cluster_score
2.7

まとめ

以上より、文のスコア付けの流れは次のようになる。

1. googleによるとgnmtは一部のケースでは人間レベルの翻訳が可能なレベルに達しているとのこと
2. google に よる と gnmt は 一部 の ケース で は 人間 レベル の 翻訳 が 可能 な レベル に 達し て いる と の こと
3. [0, 4, 6, 8, 11, 12, 14, 16]
4. [[0], [4, 6, 8], [11, 12, 14, 16]]
5. [1.0, 1.8, 2.7]
6. 2.7

スコア計算メソッドは次の通り。

def score_sentences(self, sents, important_words):
    """
    H.P. Luhn, "The Automatic Creation of Literature Abstracts"によるアプローチ
    """
    scores = []
    sent_idx = -1

    nltkjp = NLTKJP()
    for s in [nltkjp.word_tokenize(sent) for sent in sents]:
        sent_idx += 1
        word_idx = []

        # 単語リスト中の個々の単語について
        for w in important_words:
            try:
                # 文中の重要単語が出現した位置のインデックスを計算する
                word_idx.append(s.index(w))
            except ValueError, e:  # この文にはwが含まれていない
                pass
        word_idx.sort()

        # 一部の文は、重要単語を1つも含んでいないことがありえる
        if len(word_idx) == 0:
            continue

        # 単語のインデックスを使って2つの連続する単語に対して
        # 最大距離の閾値を使ってクラスタを計算する
        clusters = []
        cluster = [word_idx[0]]
        i = 1
        while i < len(word_idx):
            if word_idx[i] - word_idx[i-1] < self.CLUSTER_THRESHOLD:
                cluster.append(word_idx[i])
            else:
                clusters.append(cluster[:])
                cluster = [word_idx[i]]
            i += 1
        clusters.append(cluster)

        # 各クラスタのスコアを計算。クラスタのスコアの最大値がその文のスコア
        score_list = []
        max_cluster_score = 0
        for c in clusters:
            swc = len(c)  # significant_words_in_cluster
            twc = c[-1] - c[0] + 1  # total_words_in_cluster
            score = 1.0 * swc*swc / twc
            score_list.append(score)

            if score > max_cluster_score:
                max_cluster_score = score

        scores.append((sent_idx, max_cluster_score))

    return scores

Luhnの要約アルゴリズムの前処理

入門 ソーシャルデータ ―データマイニング、分析、可視化のテクニック

入門 ソーシャルデータ ―データマイニング、分析、可視化のテクニック

入門 ソーシャルデータ ―データマイニング、分析、可視化のテクニックの8.4章に、Luhnのプリミティブな要約アルゴリズムの解説がある。 このアルゴリズムは非常にシンプルであるものの、そこそこの精度が出せる。 解説では英語のみの対応となっていたため、日本語にも適用できるようにしてみた。

アルゴリズムは、基本的な自然言語処理を行う前処理と、Luhnの要約アルゴリズムとなる本処理、 要約を出力するための後処理に別れる。 ここではまず前処理について説明する。

要約対象テキスト

Google翻訳は人間レベルの翻訳精度を目指して人工知能を活用 の記事本文を要約対象としてみる。

text = "スマートフォンでもPCでもオフラインでも使え、100以上の言語に対応しているGoogleの翻訳サービス「Google翻訳」が、ちょうど10周年を迎えるタイミングでより自然な翻訳を可能にする人工知能(AI)を活用した「GNMT」システムを発表しました。\
AI研究で複数の大手IT企業とのパートナーシップを結んだばかりのGoogleは、AIを駆使した翻訳システム「GNMT(Google Neural Machine Translation)」を発表しました。Google翻訳ではこれまでフレーズベースで機械翻訳するPBMTという\
システムが採用されていましたが、このような単語やフレーズごとに機械的に文章を翻訳する方法ではなく、文章全体をひとつの翻訳単位として捉えることができるのがGNMTだそうです。Googleの研究者によると、「GNMTの『文章全体をひとつの翻訳単位\
として捉える』アプローチの長所は、工学的設計の選択肢がPBMTよりも少なくて済むことです」とのこと。実際にGNMTを駆使した最初の翻訳では、既存のGoogle翻訳と遜色ない翻訳精度がみられたそうです。さらに、何度も翻訳を重ねることで、\
GNMTは優れた翻訳と素早い翻訳スピードの両立が可能になっている模様。Googleによると、GNMTを用いることでGoogle翻訳は翻訳ミスを55~85%も軽減できるようになるとのこと。GoogleによるとGNMTは一部のケースでは人間レベルの翻訳が\
可能なレベルに達しているとのこと。以下のグラフは人間・GNMT・PBMTの3つによる翻訳を6段階評価して比較したもので、最も翻訳精度が高いのは人間による翻訳ですが、フランス語から英語に翻訳する場合や英語からスペイン語に翻訳する場合、\
人間とGNMTの間にそれほど大きな差は存在しないそうです。"

テキストを文に分割

自作の日本語の自然言語処理ライブラリー nltkjp.py (詳しくはここここを参照) に、「。」で分割してリスト化するだけの処理を行うメソッド sent_tokenize() を追加した。

def sent_tokenize(self, doc):
    sents = [d.decode("utf-8") for d in doc.split(u'。')]

    return sents

まずはテキストをこれにかける。

nltkjp = NLTKJP()

# テキストを文に分割
sents = nltkjp.sent_tokenize(text)
>>> for sent in sents:
>>>     print sent
スマートフォンでもPCでもオフラインでも使え、100以上の言語に対応しているGoogleの翻訳サービス「Google翻訳」が、ちょうど10周年を迎えるタイミングでより自然な翻訳を可能にする人工知能(AI)を活用した「GNMT」システムを発表しました
AI研究で複数の大手IT企業とのパートナーシップを結んだばかりのGoogleは、AIを駆使した翻訳システム「GNMT(Google Neural Machine Translation)」を発表しました
Google翻訳ではこれまでフレーズベースで機械翻訳するPBMTというシステムが採用されていましたが、このような単語やフレーズごとに機械的に文章を翻訳する方法ではなく、文章全体をひとつの翻訳単位として捉えることができるのがGNMTだそうです
...

文をすべて小文字に変換

英語の大文字を小文字に変換する。 本当は日本語の動詞もすべて基本形にするとかしないといけないのかもしれないけど、また今後。

normalized_sents = [sent.lower() for sent in sents]
>>> for ns in normalized_sents:
>>>     print ns
スマートフォンでもpcでもオフラインでも使え、100以上の言語に対応しているgoogleの翻訳サービス「google翻訳」が、ちょうど10周年を迎えるタイミングでより自然な翻訳を可能にする人工知能(ai)を活用した「gnmt」システムを発表しました
ai研究で複数の大手it企業とのパートナーシップを結んだばかりのgoogleは、aiを駆使した翻訳システム「gnmt(google neural machine translation)」を発表しました
google翻訳ではこれまでフレーズベースで機械翻訳するpbmtというシステムが採用されていましたが、このような単語やフレーズごとに機械的に文章を翻訳する方法ではなく、文章全体をひとつの翻訳単位として捉えることができるのがgnmtだそうです
...

各文を単語に分割して1つにまとめる

各文章を分かち書きにしてすべてまとめる。

words = [word.lower() for sent in normalized_sents for word in nltkjp.word_tokenize(sent)]
>>> for word in words[:10]:
>>>     print word
スマート
フォン
で
も
pc
で
も
オフライン
で
も
...

頻出単語を抽出する

ここで本家のnltkが登場。FreqDistに単語のリストを入れると、単語をカウントしてソートしてくれる。

fdist = nltk.FreqDist(words)
>>> for k,v in fdist.items()[:10]:
>>>     print k, '\t', v
printの    24
翻訳  23151414131211
gnmt    10
google  10
...

ストップワードでない上位N個の単語を抽出する

頻出単語からストップワードを除き、そのうちの上位N個を取得する。 100にしてるのは、解説がそうだったからだけど、今回の例では多い気がする。 ストップワードの取得については、以下の結果に"("が含まれていたりとまだ不備があるけど、アルゴリズム簡易的な日本語ストップワードの取得メソッドを参照。 ただし、今回はストップワードの品詞は名詞以外としている。

stopwords = nltkjp.stopwords(text)
N = 100
top_n_words = [w[0] for w in fdist.items() if w[0] not in stopwords][:N]
>>> for tnw in top_n_words[:10]:
>>>     print tnw
翻訳
gnmt
google
人間
ai
pbmt
システム
可能
文章
(
...

まとめ

以上が前処理となる。 各処理をまとめると以下のようになる。

import nltk
from nltkjp import NLTKJP

N = 100  # 考慮する単語の数

def main(text):
    nltkjp = NLTKJP()

    # テキストを文に分割
    sents = nltkjp.sent_tokenize(text)

    # 文をすべて小文字に変換
    normalized_sents = [sent.lower() for sent in sents]

    # 各文を単語に分割して1つにまとめる
    words = [word.lower() for sent in normalized_sents for word in nltkjp.word_tokenize(sent)]

    # 頻出単語を抽出する
    fdist = nltk.FreqDist(words)

    # ストップワードでない上位N個の単語を抽出する
    stopwords = nltkjp.stopwords(text)
    top_n_words = [w[0] for w in fdist.items() if w[0] not in stopwords][:N]