言語処理100本ノック with Python【第1章】

Djangoの勉強で少し触れた程度で、内包表記やlambdaなどのThe Python! なコードを全然書いていないので、勉強がてら言語処理100本ノックを少しずつやっていく自分用まとめ記事です。

Quitaに結構記事があがっていたのでそちらをぱくり参考にしつつ、学んだ部分についてメモっていきます。
少しずつ記事を追記します(最初の投稿日は2018/4/1)。
100問終わるのはいつになるやら…。


このサイトの問題を解いていきます。問題文の引用はこのサイトからしています。
www.cl.ecei.tohoku.ac.jp

他の引用は各問題文の下にある参考サイトからしています。

Jupyterで書いたコードはgithubに上げています。
github.com



No 日付 学んだこと
00 4/1 スライス
01 4/2 -
02 4/3 zip()、join()、functools.reduce()
03 4/3 split()、内包表記、isalpha()、translate()、maketrans()、
compile()、sub()
04 4/4 OrderDict()
05 4/5 range()、stript()
06 4/6 set型
07 4/6 string.Template
08 4/6 islower()、chr()、ord()
08 4/7 random.shuffle()

準備運動

テキストや文字列を扱う題材に取り組みながら,プログラミング言語のやや高度なトピックを復習します.

00. 文字列の逆順

文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

素人の言語処理100本ノック:00
言語処理100本ノック with Python(第1章)

word= 'stressed'
result = word[::-1] # = word[-1::-1]
print(result)
# desserts
スライスの使い方

文字列[開始インデックス : 終了インデックス : ステップ数]


01. 「パタトクカシーー」

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

素人の言語処理100本ノック:00

word= 'パタトクカシーー'
result = word[::2]
print(result)
# パトカー

尚、タクシーを取り出す場合は「word[1::2]」とスタート位置をずらせばよい。

02. 「パトカー」+「タクシー」=「パタトクカシーー」

「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

素人の言語処理100本ノック:02

解法1

police_car = 'パトカー'
taxi = 'タクシー'
result = ''

for (a, b) in zip(police_car, taxi): # zipはイテレータを返すので中身を見るときはリストにする
    result += a + b
print(result)
# パタトクカシーー

zipの詳しい使い方は参考にしたQiitaの記事にいろいろあった。
素数は短い方に合わせる。

イテレータ

次の要素に繰り返しアクセスをするインターフェイスのこと。
つまりリスト、タプル、辞書等。

解法2

内包表記:pythonの内包表記を少し詳しく

# 文字列をつなげていると、ループを回すごとに新しいメモリを確保するので遅くなるらしい。
# 文字列のリストを作ってから、最後にjoinするver.
police_car = 'パトカー'
taxi = 'タクシー'
print(''.join([char1 + char2 for char1, char2 in zip(police_car, taxi)]))
join

'間に挿入する文字列'.join([連結したい文字列のリスト])

joinでリストを連結する場合はすべて文字列である必要がある。そうでない場合はmapを使う。

map

map(関数, リスト)

str_list = ['python', 'list', 125]
maped_list = map(str, str_list) # 全てstr型のリストを作成

解法3

from functools import reduce

police_car = 'パトカー'
taxi = 'タクシー'
result = ''.join(reduce(lambda x, y: x + y, zip(police_car, taxi)))
print(result)
functools.reduce

指定したイテラブルに対して指定した関数を累積的に実行し、結果を1つにまとめてくれるもの

直前の結果とイテラブルから取り出した値の2つを関数に渡して新たな結果を作り、これをイテラブルの終わりまで順次繰り返してくれます。

>>> def add(x, y):
...     return x+y
...
>>> import functools
>>> functools.reduce(add, [1,2,3])
6 # 0+1+2+3
>>> functools.reduce(add, [1,2,3], 1) # 初期値1を設定
7 # 1+1+2+3

勉強になります。。


03. 円周率

"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.

素人の言語処理100本ノック:03
言語処理100本ノック with Python(第1章)
Python で言語処理100本ノック2015

解法1

words = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
words = words.replace('.', "")
words = words.replace(',', "")
words = words.split() # 引数に何も指定しない場合、スペースやタブ等で自動的に区切る
result = []

# for word in words:
#     result.append(len(word))

result = [len(word) for word in words] # 上のfor文を内包表記で書き換え
  
print(result)
# [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]
内包表記

forの内包表記でリストの場合は[]で囲み、辞書の場合は{}で囲む

dic = {str(i):i for i in range(10)}

# dic = {}
# for i in range(10):
#     dic[str(i)] = i

内包表記のあれやこれ。

解法2

target = 'Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.'
result = []

words = target.split(' ')
for word in words:
    count = 0
    for char in word:
        if char.isalpha():
            count += 1
    result.append(count)

print(result)
文字判定メソッド一覧
メソッド 説明
isalnum() 文字がすべて英数文字かどうかの判定
isalpha() 文字がすべて英字かどうかの判定
isdigit() 文字がすべて数字かどうかの判定
islower() 大小文字の区別がある文字がすべて小文字かどうかの判定
isspace() 文字がすべて空白かどうかの判定
istitle() 文字列がタイトルケースかどうかの判定

isalnum()で日本語が正しく判定出来ないようだったけどエンコードすればおk。
[修正] Python 文字列の英数字判定でハマった

test = 'てすと'
test.isalnum() # True

test = 'てすと'.encode('utf-8')
test.isalnum() # False

解法3

words = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
print([len(word.translate(word.maketrans("","",",."))) for word in words.split()])
maketrans、translate

解法1では、replace()でコンマやピリオドを置き換え・削除したが、複数文字を対象にする場合には、translate()を使う。処理時間も早いらしい(下記リンク参考)。
文字の変換にはstr.translate()が便利

まず、maketransを使ってマッピング変換テーブルを作成し、その後translateに引数として渡す。
しかしPythonといえばワンライナー

translate(maketrans('変換前の文字列', '変換後の文字列', ‘削除対象の文字列’))

今回は変換はせず単純に削除対象だけを指定している。

最初、replace()の感覚で「、。」を消そうと思って以下のようにしたらエラーが出た。
変換前と変換後、二つのパラメータは同じ長さでなくてはならないようです。

print([len(word.translate(word.maketrans(",.",""))) for word in words.split()])
# ValueError: the first two maketrans arguments must have equal length
map

この問題とは関係ないけど、mapの文字置換の方法も例としてあったのでメモ。

text = "あいう、えお。"
table = {
    '、': ',',
    '。': '.',
}

result = ''.join(table.get(c, c) for c in text)

解法4

import re
s = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
pat = re.compile('[.,]')
print([len(pat.sub('', word)) for word in s.split()])

print([len(re.sub('[.,]','', word)) for word in s.split()]) # compileしない版
re.compile

正規表現を使う時は、再検索のときにパターンの指定を省略し、高速に動作させるために事前にパターンをコンパイルする。
先頭に「r」を付けると文字列中のバックスラッシュをそのままバックスラッシュとして使える。djangoのurlsでよく見たやーつ(今はver2.0だからもう見ないけど)。
そして、sub()はマッチした部分を他の文字列に置換することが出来る。
どっちでも動いてるけど最初の正規表現はcompileしてたら省略していいのかな?

置換後の文字列 = re.sub(正規表現, 置換する文字列, 置換される文字列 [, 置換回数])

Pythonでの正規表現の使い方



04. 元素記号

"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

素人の言語処理100本ノック:04
enumrate:
Python zipとenumerateの使い方

解法1

s = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
num_list = [1, 5, 6, 7, 8, 9, 15, 16, 19]

s = s.replace(".", "")
words = s.split(" ")
result = {}

for i, word in enumerate(words,1): # iの開始位置を1に指定
    if i in num_list:
        result[word[:1]] = i
    else:
        result[word[:2]] = i

print(result)
# {'H': 1, 'He': 2, 'Li': 3, 'Be': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'Ne': 10, 'Na': 11, 'Mi': 12, 'Al': 13, 'Si': 14, 'P': 15, 'S': 16, 'Cl': 17, 'Ar': 18, 'K': 19, 'Ca': 20}

解法2(for分内を内包表記で)

for i, word in enumerate(words, 1):
    j = 1 if i in num_list else 2
    result[word[:j]] = i
OrderDict

Pythonの辞書で入れた順番を管理する

words = OrderedDict((('one', 1), ('two', 2)))

のようにタプルを引数に渡すと良い。
python OrderedDict便利だが注意が必要 | Shannon Lab



05. n-gram

与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

素人の言語処理100本ノック:05
言語処理100本ノック 第1章 in Python
n-gramとは

n-gramとは、任意の文書や文字列などにおける任意のn文字が連続した文字列のことである。
1文字続きのものはunigram、2文字続きのものはbigram、3文字続きのものはtrigram、と特に呼ばれ、4文字以上のものは、単に4-gram、5-gramと表現されることが多い。

n-gramとは - IT用語辞典 Weblio辞書
N-gram - Negative/Positive Thinking

解法1

# def n_gram(target, n):
#     result = []
#     for i in range(0, len(target) - n + 1):
#         result.append(target[i:i + n])
#     return result

def n_gram(s,n): return [s[i:i+n] for i in range(len(s)-n+1)]

target = 'I am an NLPer'
words_target = target.split(' ')

print(n_gram(target, 2)) # 文字bi-gram
print(n_gram(words_target, 2)) # 単語bi-gram
# ['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']
# [['I', 'am'], ['am', 'an'], ['an', 'NLPer']]

解法2

target="I am an NLPer"

#文字bi-gram
charGram=[target[i:i+2] for i in range(len(target)-1)]

#単語bi-gram
words=[word.strip(".,") for word in target.split()]
wordGram=["-".join(words[i:i+2]) for i in range(len(words)-1)]

print(charGram)
print(wordGram)

内包表記、、慣れないなぁ。。
pythonの内包表記を少し詳しく

内包表記の方が通常のfor文とかより早いらしい。あとこんな計測の仕方があるのか~と勉強になった。
Pythonのリスト内包表記の速度

Pythonって読みやすいことが利点!っていうけど内包表記読みにくくね??と思ってたら面白い記事を発見。
Pythonのリスト内包表記をdisる - shkh's blog




06. 集合

"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

素人の言語処理100本ノック:06

set型

集合にはset型を使う。リストと同じく複数の値を格納できるが違いは、

・重複した要素がない
・要素に順序がない

重複する要素を追加することも出来るが(.add())、エラーは発生せずに無視される。
要素の削除はremove()、全削除はclear()。

解法

paradise = 'paraparaparadise'
paragraph = 'paragraph'

def n_gram(s,n): return [s[i:i+n] for i in range(len(s)-n+1)]

X = set(n_gram(paradise ,2))
Y = set(n_gram(paragraph ,2))

print(X | Y) # =  X.union(Y)
print(X & Y) # =  X.intersection(Y)
print(X - Y) # =  X.difference(Y)

print('se' in X)
print('se' in Y)

# {'pa', 'se', 'ar', 'gr', 'ph', 'ra', 'ag', 'ad', 'ap', 'di', 'is'}
# {'ar', 'ap', 'pa', 'ra'}
# {'ad', 'di', 'se', 'is'}
# True
# False

07. テンプレートによる文生成

引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.

素人の言語処理100本ノック:07

解法1

def create_sentense(x, y, z):
    return ('{0}時の{1}は{2}'.format(x,y,z))

print(create_sentense(12, "気温", 22.4))
# 12時の気温は22.4

急にえらく簡単だな、と思って参考のQiitaの記事を見たらstring.Templateクラスというものがあるらしい。

解法2

from string import Template

def create_sentense(x, y, z):
    s = Template('$hour時の$targetは$value')
    return s.substitute(hour=x, target=y, value=z) # テンプレート置換を行い新たな文字列を形成

print(create_sentense(12, "気温", 22.4))

6.1. string — 一般的な文字列操作 — Python 3.6.5 ドキュメント

文字列置換は「string substitution」というらしい。覚えておこう。



08. 暗号文

与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.
 ・英小文字ならば(219 - 文字コード)の文字に置換
 ・その他の文字はそのまま出力
この関数を用い,英語のメッセージを暗号化・復号化せよ.

素人の言語処理100本ノック:08


おっ、03でついでにメモっておいたislower()の出番!

解法

def cipher(sentense):
    result = ''
    for c in sentense:
        if c.islower():
            result += chr(219-ord(c))
        else:
            result += c
    return result

print(cipher('hitono okanede yakiniku tabetai!!'))
print(cipher(cipher('hitono okanede yakiniku tabetai!!')))

# srglml lpzmvwv bzprmrpf gzyvgzr!!
# hitono okanede yakiniku tabetai!!
chr()、ord()

以下のコードでわかる通りchr()は引数の数字(文字コード)から文字を、
ordはその逆で文字から文字コードを返す。

print(chr(97)) # a
print(ord('a')) # 97

09. Typoglycemia

スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え,その実行結果を確認せよ.

素人の言語処理100本ノック:09
言語処理100本ノック with Python(第1章)


Typoglycemiaってそもそも何だろう。

Typoglycemiaとは、単語を構成する文字を並べ替えても、最初と最後の文字が合っていれば読めてしまう現象のことである。

Typoglycemiaとは (タイポグリセミアとは) [単語記事] - ニコニコ大百科

あ~。だから先頭と末尾は入れ替えちゃだめなのね。

解法1

import random

words = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."

def typoglycemia(words):
    result = []
    for word in words.split():
        if len(word) <= 4:
            result.append(word)
        else:
            chr_list = list(word[1:-1])
            random.shuffle(chr_list)
            result.append(word[0] + ''.join(chr_list) + word[-1])
            
    return ' '.join(result)

print(typoglycemia(words))
print(type(typoglycemia(words)))
# I clduno't bleviee that I cluod atlcaluy uaresndntd what I was ranedig : the peamhnneol pwoer of the hmaun mind .
# <class 'str'>

解法2

import random
words = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."

def typoglycemia(word):
    if len(word) <= 4: return word    
    typo = list(word)[1:-1]
    random.shuffle(typo)
    return word[0] + ''.join(typo) + word[-1]

print(' '.join(list(map(typoglycemia, words.split()))))