2018年09月17日

言語処理100本ノックでPython入門 #70 - 機械学習、データの入手・整形



今日から言語処理100本ノック 2015の第8章・機械学習に突入です。scikit-learnを使って解いていく予定ですが、1問目はscikit-learnとは直接は関係ない問題です。いわゆる前処理ってやつですね。

■ 問題
本章では,Bo Pang氏とLillian Lee氏が公開しているMovie Review Dataのsentence polarity dataset v1.0を用い,文を肯定的(ポジティブ)もしくは否定的(ネガティブ)に分類するタスク(極性分析)に取り組む.

70. データの入手・整形
文に関する極性分析の正解データを用い,以下の要領で正解データ(sentiment.txt)を作成せよ.
  1. rt-polarity.posの各行の先頭に"+1 "という文字列を追加する(極性ラベル"+1"とスペースに続けて肯定的な文の内容が続く)
  2. rt-polarity.negの各行の先頭に"-1 "という文字列を追加する(極性ラベル"-1"とスペースに続けて否定的な文の内容が続く)
  3. 上述1と2の内容を結合(concatenate)し,行をランダムに並び替える
sentiment.txtを作成したら,正例(肯定的な文)の数と負例(否定的な文)の数を確認せよ.

■ どうやって解くか

どうしようか悩みましたが、入力ファイルは極性ラベルを付けていったんリストに格納します。 AddPlus, AddMinus がそのメソッドです。

で、この2つをさらに + 演算子で結合し、そのあと、random.shiftでランダムに並び替えます。 これを、sentiment.txtに出力します。

今回のトピックは、+ 演算子でのリストの連結と、random.shiftによるランダムな並び替えの2つ。 第8章: 機械学習の1問目は、今までの延長でなんとかなりました。

それと、入力ファイルが、UTF-8ではありませんでした。なので、encoding='cp1252' のオプションを付けて読み込んでいます。

■ Pythonのソース

import random

def AddPlus():
    lines = []
    with open('rt-polarity.pos', 'r', encoding='cp1252') as fin:
        for line in fin:
            lines.append('+1 ' + line)
    return lines

def AddMinus():
    lines = []
    with open('rt-polarity.neg', 'r', encoding='cp1252') as fin:
        for line in fin:
            lines.append('-1 ' + line)
    return lines

def CreateText():
    lines = AddPlus() + AddMinus()
    random.shuffle(lines)
    with open('sentiment.txt', 'w') as fout:
        fout.writelines(lines)


def main():
    CreateText()

if __name__ == '__main__':
    main()

■ 結果

出力ファイルの先頭8行を載せます。

+1 a wonderful character-based comedy . 
-1 a ragbag of cliches . 
+1 gently humorous and touching . 
-1 the first fatal attraction was vile enough . do we really need the tiger beat version ? 
-1 shafer's feature doesn't offer much in terms of plot or acting . 
-1 the material and the production itself are little more than routine . 
+1 it's a beautifully accomplished lyrical meditation on a bunch of despondent and vulnerable characters living in the renown chelsea hotel . . . 
+1 captivates and shows how a skillful filmmaker can impart a message without bludgeoning the audience over the head . 
  

Posted by gushwell at 22:30Comments(0)Python

2018年09月05日

言語処理100本ノックでPython入門 #69 - FlaskでWebアプリケーションを作成



とうとう言語処理100本ノック 2015の第7章・問題69です。やっと7章の最後の問題まできました。

■ 問題
69. Webアプリケーションの作成
ユーザから入力された検索条件に合致するアーティストの情報を表示するWebアプリケーションを作成せよ.アーティスト名,アーティストの別名,タグ等で検索条件を指定し,アーティスト情報のリストをレーティングの高い順などで整列して表示せよ.

PythonでWebアプリケーションってどうやって作成するのかそのそも経験がないのでわからない。 しらべたところ、DjangoとFlaskというフレームワークがあるらしい。 今回は、手軽そうなFlaskを使って問題を解いてみようと思います。
conda list flask
とやると、

flask                     0.12                     py36_0
flask-cors                3.0.2                    py36_0
って表示されたので、anacondaには、すでにflaskが入っているみたい。
なので、これを使うことにします。

■ HTMLレイアウトファイルの作成

あちこちのサイトの見よう見まねで、テンプレートファイルを作成します。
まずは、大本となるlayout.htmlファイル。

<!DOCTYPE html>
<html>
<head>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <title>言語処理100本ノック:No69</title>
    <style>
        body {
            margin: 20px;
        }
    </style>
<body>
    <div class="container-fluid">
    {% block content %}
    <!-- ここにメインコンテンツが挿入される  -->
    {% endblock %}
    </div>
</body>
</head>


とりあえず、bootstrapを使います。デザインに凝るつもりはないですが...
cssファイルを別に作るまでもないので、styleタグでbodyのmarginだけ指定しています。

■ 検索ページのhtmlファイル

次に作成するのは、検索用のhtmlファイルです。

{% extends "layout.html" %}
{% block content %}

<form method="post" class="form-horizontal">
    <div  class="form-group">
        <label class="control-label col-xs-2">名前: </label>
        <input name="name" type="text" >
    </div>
    <div  class="form-group">
        <label class="control-label col-xs-2">地域: </label>
        <input name="area" type="text" >
    </div>
    <div  class="form-group">
        <label class="control-label col-xs-2">タグ: </label>
        <input name="tag" type="text" >
    </div>
    <div  class="form-group">
        <div class="control-label col-xs-2"></div>
        <button class="btn btn-primary" type="submit">検索</button>
    </div>    
</form>
{% endblock %}
※ あれ、なんかHTMLコードの色付けがおかしいけど、許してください。

先頭と最後に、flask特有の記述がありますが、それ以外は普通のhtmlファイルです。
class名には、bootstrap専用のクラス名を指定しています。

■ 結果表示用のhtmlファイルの作成

次に結果表示用のhtmlファイルを作成します。

{% extends "layout.html" %}
{% block content %}
<h3>結果一覧</h3>
<p>
<a href="/">戻る</a>
</p>
<div class="table-responsive">
    <table class="table table-striped">
        <thead>
            <tr>
                <th>名前</th>
                <th>地域</th>
                <th>性別</th>
                <th>タグ</th>
                <th>別名</th>
                <th>タイプ</th>
            </tr>
        </thead>
        <tbody>
            {% for artist in artists %}
            <tr>
                <td>{{ artist.name }}</td>
                <td>{{ artist.area }}</td>
                <td>{{ artist.gender }}</td>
                <td>
                    {% for tag in artist['tags'] %}
                        {{ tag.value }}<br>
                    {% endfor %}
                </td>
                <td>
                    {% for alias in artist['aliases'] %}
                        {{ alias.name }}<br>
                    {% endfor %}
                </td>
                <td>{{ artist.type }}</td>
                </tr>
            {% else %}
                <td>書籍が登録されていません</td>
            {% endfor %}
        </tbody>
    </table>
</div>
{% endblock %}

{{}}でくくっているのが、モデルとバインドする項目です。

{% for artist in artists %}など、{% %}で括られているのは、flaskのテンプレートエンジンが処理するための記述です。forにelseが使えるのがいかにもPython用のフレームワークですね。

ちなみに、MongoDBから検索した結果(複数)がartistsです。これを forで回して、artist.nameとかartist['aliases']といった記述で取り出しています。まあ、artist.aliasesでもいいんですけど、気分で両方の書き方をしてみました。

このアプリは、検索ページ「検索」ボタンをしたら、その結果を表形式で表示するだけなので、定義するページはこれで終わりです。

■ Pyrthonのコード

それでは、Pythoのコードです。

import pymongo
from flask import Flask, render_template, request

class SearchInfo:
    def __init__(self, name, area, tag):
        self.name = name
        self.area = area
        self.tag = tag

class Searcher:
    def __init__(self):
        self.client = pymongo.MongoClient('localhost', 27017)
        self.db = self.client['MusicBrainzDb']
        self.co = self.db['artists']

    def search(self, info):
        query = { '$and' : [ ]}
        if info.name != '':
            cond = []
            cond.append({ 'aliases.name': info.name })
            cond.append({ 'name': info.name })
            query['$and'].append({ '$or': cond })
        if info.area != '':
            query['$and'].append({ 'area': info.area })
        if info.tag != '':
            query['$and'].append({ 'tags.value': info.tag })
        q2 = self.co.find(query).sort('rating.count', pymongo.DESCENDING)
        return list(q2)[:100]

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        searcher = Searcher()
        results = searcher.search(SearchInfo(request.form['name'], request.form['area'], request.form['tag']))
        return render_template('result.html', artists=results) 
    else:
        return render_template('search.html') 

if __name__ == "__main__":
    app.run(debug=True)

SearchInfoクラスは、Flaskとは直接は関係していないクラスなので、普通に書けばOK.

flask関連のコードは、これ以降のコードです。

app = Flask(__name__)
これで、Flaskオブジェクトを生成して、runメソッドでWebアプリを起動しています。

def index():

が、アクセスした時に呼び出される関数です。今回のアプリでは、URLは、ルート(/)だけです。

@app.route('/', methods=['GET', 'POST'])
で、get/post両方で、この関数が呼ばれるようにしています。

getならば、render_template関数を使って、search.htmlをレンダリングした結果を返しています。
postならば検索してから、result.htmlをレンダリングして返しています。このとき、
artists=results

で、htmlテンプレートのartistsと検索結果を関連付けています。

■ 実行してみる

MongoDBが起動してある前提です。
めんどくさいので、Visla Studio Codeのデバッグ機能をつかって実行ました。

コンソールに以下のようなメッセージが表示されます。

 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger pin code: 148-857-886

なので、http://127.0.0.1:5000/ にアクセス。

すると、以下のようなページが表示されるので、ここで適当に検索ワードを入れます。

スクリーンショット 2018-09-02 15.31.25
検索ボタンを押します。
すると、無事検索されて、以下の結果が得られました。
スクリーンショット 2018-09-02 15.31.49
これだけだと、ルーティング、リダイレクト、認証...などわからないことも多いですが、Flaskの基礎の基礎はわかったような気がします。まあ、HTMLテンプレートでサーバー側のモデルとバインドさせてHTMLをレンダリングするってのは、ASP.NET MVCと考え方は同じですね。ただ、Controller作る必要がないので、どう設計するかは開発者に任されているということなんですかね。実際にアプリ作成するとなるとベストプラクティスとか知る必要がありそうです。 でも、慣れないことやったので、さすがに疲れました。  
Posted by gushwell at 22:00Comments(0)Python

2018年08月29日

言語処理100本ノックでPython入門 #68 - MongoDBでソート



今日は、言語処理100本ノック 2015の第7章・問題68に挑戦です。


■ 問題
68. ソート
"dance"というタグを付与されたアーティストの中でレーティングの投票数が多いアーティスト・トップ10を求めよ.

■ Pythonのコード
import json
import pymongo

def findDance():
    client = pymongo.MongoClient('localhost', 27017)
    db = client['MusicBrainzDb']
    co = db['artists']
    return co.find({'tags.value': 'dance'}).sort('rating.count', pymongo.DESCENDING)

def main():
    for d in findDance()[0:10]:
        print(f'{} - {}'.format(d['name'], d['rating']['count']))
        #print(f'{d['name']} - {d['rating']['count']}')

if __name__ == '__main__':
    main()

■ ちょと説明

まずは、検索ですが、以下のようにオブジェクトを引数に渡して検索することができます。

co.find({'tags.value': 'dance'})
面白いですね。
そして、以下のように、ドットで繋げてsortすることができます。
co.find({'tags.value': 'dance'}).sort('rating.count', pymongo.DESCENDING)

pymongo.DESCENDINGを指定しているので、降順にソートしています。

取得したオブジェクトの各値にアクセスするには、辞書型なので、d['name']のように書けば、値を取得できます。階層構造になっている場合は、

d['rating']['count'])

のようにアクセスします。

これを使って、結果を以下のコードで画面に表示しています。

print('{} - {}'.format(d['name'], d['rating']['count']))

■ 結果
Madonna - 26
Bjork - 23
The Prodigy - 23
Rihanna - 15
Britney Spears - 13
Maroon 5 - 11
Adam Lambert - 7
Fatboy Slim - 7
Basement Jaxx - 6
Cornershop - 5
  
Posted by gushwell at 22:00Comments(0)Python

2018年08月26日

言語処理100本ノックでPython入門 #67 - MongoDBで複数ドキュメントの取得



今日は、言語処理100本ノック 2015の第7章・問題67を解きます。

■ 問題
67. 複数のドキュメントの取得
特定の(指定した)別名を持つアーティストを検索せよ.

■ Pythonのコード

import json
import pymongo
from bson.objectid import ObjectId

def ObjectIdToStr(o):
    if isinstance(o, ObjectId):
        return str(o)
    raise TypeError(repr(o) + " is not sported")

def findAlias(alias):
    client = pymongo.MongoClient('localhost', 27017)
    db = client['MusicBrainzDb']
    co = db['artists']
    return co.find({'aliases.name': alias})

def printArtist(artist):
    print('')
    print(json.dumps(artist, indent=4, ensure_ascii=False, sort_keys=True, default=ObjectIdToStr))

def main():
    alias = input('=>')
    for d in findAlias(alias):
        printArtist(d)

if __name__ == '__main__':
    main()

■ ちょと説明

ireturn co.find({'aliases.name': alias})
で検索結果を返しています。このメソッドは、条件に合うデータが複数あれば、複数のアーティストのオブジェクトが返ります。 

これをprintArtist関数で表示しています。MongoDBから取得したデータはjsonなので、json.dumpsで整形させて表示させています。


■ 実行結果

実行例では、取得したデータは一つだけですが、入力した別名と一致するアーティストが複数いれば、複数のアーティストのデータが表示されます。
=>サザン

{
    "_id": "5a653c60323575566c9d9167",
    "aliases": [
        {
            "name": "Southern All Stars",
            "sort_name": "Southern All Stars"
        },
        {
            "name": "SAS .",
            "sort_name": "SAS ."
        },
        {
            "name": "サザン",
            "sort_name": "サザン"
        }
    ],
    "area": "Japan",
    "begin": {
        "year": 1975
    },
    "ended": true,
    "gid": "24d9e513-c215-4901-87ee-b66aa769ab9d",
    "id": 9549,
    "name": "サザンオールスターズ",
    "rating": {
        "count": 2,
        "value": 70
    },
    "sort_name": "Southern All Stars",
    "tags": [
        {
            "count": 1,
            "value": "japanese"
        },
        {
            "count": 1,
            "value": "asian"
        },
        {
            "count": 1,
            "value": "fixme label mess"
        }
    ],
    "type": "Group"
}
  
Posted by gushwell at 22:00Comments(0)Python

2018年08月22日

言語処理100本ノックでPython入門 #66 - MongoDBで検索件数の取得



今日は、言語処理100本ノック 2015の第7章・問題66を解きます。

■ 問題
66. 検索件数の取得
MongoDBのインタラクティブシェルを用いて,活動場所が「Japan」となっているアーティスト数を求めよ.

■ Pythonのコード
import pymongo

def countJapan():
    client = pymongo.MongoClient('localhost', 27017)
    db = client['MusicBrainzDb']
    co = db['artists']
    count = co.find({'area': 'Japan'}).count()
    print(count)

def main():
    countJapan()

if __name__ == '__main__':
    main()

■ ほんの少し説明

今回は簡単でした。単に、findメソッドと、countメソッド使うだけでした。
count = co.find({'area': 'Japan'}).count()

のように、ドットで連結できる。ちょとC#のLINQっぽい。


■ 実行結果
22821
  
Posted by gushwell at 21:30Comments(0)Python

2018年08月19日

言語処理100本ノックでPython入門 #65 - MongoDBの検索



今日は、言語処理100本ノック 2015の第7章・問題65を解きます。


■ 問題
65. MongoDBの検索
MongoDBのインタラクティブシェルを用いて,"Queen"というアーティストに関する情報を取得せよ.さらに,これと同様の処理を行うプログラムを実装せよ.
「MongoDBのインタラクティブシェルを用いて」という部分は省略、後半のプログラムを作成するほうだけ考えます。

■ Pythonのコード
import json
import pymongo
from bson.objectid import ObjectId

def ObjectIdToStr(o):
    if isinstance(o, ObjectId):
        return str(o)
    raise TypeError(repr(o) + " is not suported")

def find(name):
    client = pymongo.MongoClient('localhost', 27017)
    db = client['MusicBrainzDb']
    co = db['artists']
    return co.find({'name': name})

def printArtist(artist):
    print('')
    print(json.dumps(artist, indent=4, ensure_ascii=False, sort_keys=True, default=ObjectIdToStr))

def main():
    for d in find('Queen'):
        printArtist(d)

if __name__ == '__main__':
    main()

■ 簡単に説明


find メソッドを使うと、該当するデータを(複数ある場合はすべて)取得できます。この結果をforで回しています。

取得したデータはJSON形式です。これをdumpメソッドで文字列にして表示しています。この時、
json.dumps(d, indent=4, ensure_ascii=False, sort_keys=True, default=ObjectIdToStr)
と、パラメータでensure_ascii=False としないと、以下のように
    "aliases": [
        {
            "name": "\u5973\u738b",
            "sort_name": "\u5973\u738b"
        }
    ],
漢字部分が、"\u5973"といった形式でエンコードされてしまいます。

それと、ObjectId()は、そのままではdumpsできないので、default= で変換ルーチンを登録しています。 これをやらないと、以下の例外が発生してしまいます。
TypeError: Object of type 'ObjectId' is not JSON serializable

ちなみに、ObjectIdToStr関数の中で使っている repr()組み込み関数は、オブジェクトの印字可能な表現を含む文字列を返します。

■ 結果
{
    "_id": "5b73dc59b520d9c8477aafe2",
    "aliases": [
        {
            "name": "Queen",
            "sort_name": "Queen"
        }
    ],
    "area": "Japan",
    "ended": true,
    "gender": "Female",
    "gid": "420ca290-76c5-41af-999e-564d7c71f1a7",
    "id": 701492,
    "name": "Queen",
    "sort_name": "Queen",
    "tags": [
        {
            "count": 1,
            "value": "kamen rider w"
        },
        {
            "count": 1,
            "value": "related-akb48"
        }
    ],
    "type": "Character"
}

{
    "_id": "5b73dcd0b520d9c8477b768e",
    "aliases": [
        {
            "name": "女王",
            "sort_name": "女王"
        }
    ],
    "area": "United Kingdom",
    "begin": {
        "date": 27,
        "month": 6,
        "year": 1970
    },
    "ended": true,
    "gid": "0383dadf-2a4e-4d10-a46a-e9e041da8eb3",
    "id": 192,
    "name": "Queen",
    "rating": {
        "count": 24,
        "value": 92
    },
    "sort_name": "Queen",
    "tags": [
        {
            "count": 2,
            "value": "hard rock"
        },
        {
            "count": 1,
            "value": "70s"
        },
        {
            "count": 1,
            "value": "queen family"
        },
        {
            "count": 1,
            "value": "90s"
        },
        {
            "count": 1,
            "value": "80s"
        },
        {
            "count": 1,
            "value": "glam rock"
        },
        {
            "count": 4,
            "value": "british"
        },
        {
            "count": 1,
            "value": "english"
        },
        {
            "count": 2,
            "value": "uk"
        },
        {
            "count": 1,
            "value": "pop/rock"
        },
        {
            "count": 1,
            "value": "pop-rock"
        },
        {
            "count": 1,
            "value": "britannique"
        },
        {
            "count": 1,
            "value": "classic pop and rock"
        },
        {
            "count": 1,
            "value": "queen"
        },
        {
            "count": 1,
            "value": "united kingdom"
        },
        {
            "count": 1,
            "value": "langham 1 studio bbc"
        },
        {
            "count": 1,
            "value": "kind of magic"
        },
        {
            "count": 1,
            "value": "band"
        },
        {
            "count": 6,
            "value": "rock"
        },
        {
            "count": 1,
            "value": "platinum"
        }
    ],
    "type": "Group"
}

{
    "_id": "5b73de48b520d9c8477d30e6",
    "ended": true,
    "gid": "5eecaf18-02ec-47af-a4f2-7831db373419",
    "id": 992994,
    "name": "Queen",
    "sort_name": "Queen"
}
  
Posted by gushwell at 21:30Comments(0)Python