言語処理100本ノック 2020 第1章: 準備運動

先日，[言語処理100本ノック2020](https://nlp100.github.io/ja/)が公開されました．私自身，自然言語処理を初めてから1年しか経っておらず，細かいことはよくわかっていませんが，技術力向上のために全ての問題を解いて公開していこうと思います．

Qiitaの記事は初めて書きます．なにもわかりません．

すべてjupyter notebook上で実行するものとし，問題文の制約は都合よく破っていいものとします．
ソースコードはgithubにもあります．[あります](https://github.com/nymwa/nlp100/blob/master/chapter01.ipynb)．

環境はPython3.8.2とUbuntu18.04です．

# 第1章: 準備運動

## 00. 文字列の逆順
> 文字列”stressed”の文字を逆に（末尾から先頭に向かって）並べた文字列を得よ

```python:コード
x = 'stressed'
x = x[::-1]
x
```

```python:出力
'desserts'
```

スライス操作ですね．`[開始位置:終了位置:ステップ幅]`のステップ幅を負の値にすると，逆順に切り出してくれます．

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

パタトクカシーー，ググったところ元ネタはピタゴラスイッチらしい．

```python:コード
x = 'パタトクカシー'
x = x[::2]
x
```

```python:出力
'パトカー'
```

1,3,5,7番目の文字を順に取り出していってもいいとは思うけど，スライス操作を使ったら楽ですね．という気持ちなんだと思われる．

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

複数のリストの同じ位置にある要素をリストにまとめる操作は，`zip`関数で実現されます．
パトカーとタクシーは同じ長さなのでいいんですが，長さが異なる場合，`zip`が生成するリスト(正確にはイテレータ)は短い方のリストの長さに合わせてしまいます．長いほうの最後のほうの文字がなかったことにされてしまう...

`itertools`の`zip_longest`を使えば長い方に合わせてくれます．わあい．

```python:コード
from itertools import zip_longest
```

```python:コード
x1 = 'パトカー'
x2 = 'タクシー'
x = [
    char
    for two_chars in zip_longest(x1, x2, fillvalue = '')
    for char in two_chars
]
x = ''.join(x)
x
```

```python:出力
'パタトクカシーー'
```

`zip_longest`は短い方のリストの末尾をデフォルトでは`None`で埋めてしまうので，`fillvalue = ''`で空文字を埋めるようにします．
リスト内包記法内のfor文が２つあるのは二重ループです．実際にforループを２重に書いたものが同じ動作をすることを確認したことがあれば，何も難しいところはないと感じるはずです．

## 03. 円周率
> “Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.”という文を単語に分解し，各単語の（アルファベットの）文字数を先頭から出現順に並べたリストを作成せよ

円周率の覚え歌です．普通に覚えた方が楽です．

```python:コード
import re
```

```python:コード
x = 'Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.'
x = re.sub(r'[^\w\s]', '', x)
x = x.split(' ')
x = [len(word) for word in x]
x
```

```python:出力
[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]
```

まず正規表現を用いてラテン文字と空白以外を取り除きます．そして，空白区切りにして，書く単語の長さを求めます．
これは人間が読める正規表現という感じですね．魔術を習ったことはないので，技巧的な正規表現は書けません．

## 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文字を取り出し，取り出した文字列から単語の位置（先頭から何番目の単語か）への連想配列（辞書型もしくはマップ型）を作成せよ．

```python:コード
x = 'Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.'
x = x.split(' ')
idx = {1, 5, 6, 7, 8, 9, 15, 16, 19}
d1 = [
    (num + 1, word[:1])
    for num, word in enumerate(x)
    if num + 1 in idx
]
d2 = [
    (num + 1, word[:2])
    for num, word in enumerate(x)
    if num + 1 not in idx
]
dct = {name:num for num, name in d1 + d2}
dct
```

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

入力をスペース毎に区切り，指定された番地のトークンを前から1文字か2文字切り取って，番地と元素記号のタプルにします．
それで，そのタプルを辞書に格納していきます．
Pythonのリストの番地は0から数えるので，アクセスするとき元素番号と1ずれることに気をつけるぐらいで，特に難しいことはないですね．

マグネシウムがMiになっちゃってますね．

## 05. n-gram
> 与えられたシーケンス（文字列やリストなど）からn-gramを作る関数を作成せよ．この関数を用い，”I am an NLPer”という文から単語bi-gram，文字bi-gramを得よ．

n-gramの作り方は以下の記事にあるものが好きです．

[Pythonでお手軽かつ速めなngram](https://qiita.com/shuntaroy/items/b0e557be3406a9b09ade)

```python:コード
def ngram(n, lst):
    return list(zip(*[lst[i:] for i in range(n)]))
```

```python:コード
chars = 'I am an NLPer'
char_bi_gram = ngram(2, chars)

words = chars.split(' ')
word_bi_gram = ngram(2, words)

print('文字 bi-gram', char_bi_gram)
print('単語 bi-gram', word_bi_gram)
```

```python:出力
文字 bi-gram [('I', ' '), (' ', 'a'), ('a', 'm'), ('m', ' '), (' ', 'a'), ('a', 'n'), ('n', ' '), (' ', 'N'), ('N', 'L'), ('L', 'P'), ('P', 'e'), ('e', 'r')]
単語 bi-gram [('I', 'am'), ('am', 'an'), ('an', 'NLPer')]
```

上記のようにすると，文字列とリストのn-gramを同じ関数で実現できるので，便利です．

`ngram`関数の引数の順番は，`functools.partial`を適用して個別のn-gramを求める関数を作ることを想定して決めました．

```python:コード
from functools import partial
```

```python:コード
bigram = partial(ngram, 2)
bigram(chars)
```

```python:出力
[('I', ' '),
 (' ', 'a'),
 ('a', 'm'),
 ('m', ' '),
 (' ', 'a'),
 ('a', 'n'),
 ('n', ' '),
 (' ', 'N'),
 ('N', 'L'),
 ('L', 'P'),
 ('P', 'e'),
 ('e', 'r')]
```

bi-gramを求める関数を作ることができました．

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

```python:コード
str1 = 'paraparaparadise'
str2 = 'paragraph'
X = set(ngram(2, str1))
Y = set(ngram(2, str2))
print('積集合', X & Y)
print('差集合', X - Y)
print('se in X?', ('s', 'e') in X)
print('se in Y?', ('s', 'e') in Y)
```

```python:出力
積集合 {('r', 'a'), ('p', 'a'), ('a', 'r'), ('a', 'p')}
差集合 {('a', 'd'), ('s', 'e'), ('i', 's'), ('d', 'i')}
se in X? True
se in Y? False
```

集合の操作です．`and`とか`or`が使えそうにも思えますが，使えません．`&`や`|`を使うことになりますが，`union`，`intersection`なども用意されています．`<=`で部分集合かの判定ができたりと，なんかいろいろあります．

## 07. テンプレートによる文生成
> 引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ．さらに，x=12, y=”気温”, z=22.4として，実行結果を確認せよ．

```python:コード
def temperature(x, y, z):
    return '{}時の{}は{}'.format(x, y, z)
```

```python:コード
temperature(12, '気温', 22.4)
```

```python:出力
'12時の気温は22.4'
```

文字列フォーマットです．`{}`の中にいろいろ書き込むことで，表示の形式を操作できたりします．
複雑な操作を必要としないならば，`f'{x}時の{y}は{z}'`という表記も可能です．
`%`演算子を使う方法もありますが，`printf`みたいな書き方がしたい場合(?)以外使わないし，いいかなという感じがあります．

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

シーザー暗号ですね．

英語のメッセージとして[The quick brown fox jumps over the lazy dog](https://ja.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog)を使ってみます．

```python:コード
def cipher(xs):
    xs = [
        chr(219 - ord(x)) if x.islower() else x
        for x in xs
    ]
    return ''.join(xs)
```

```python:コード
x = 'The quick brown fox jumps over the lazy dog. 1234567890'
print('平文', x)
x = cipher(x)
print('暗号文', x)
x = cipher(x)
print('復号文', x)
```

```python:出力
平文 The quick brown fox jumps over the lazy dog. 1234567890
暗号文 Tsv jfrxp yildm ulc qfnkh levi gsv ozab wlt. 1234567890
復号文 The quick brown fox jumps over the lazy dog. 1234567890
```

文字列をASCIIコードに変換して，暗号化を適用し，文字列へと戻すという操作を実装する問題です．
`ord`と`chr`の使い方を知っていること，`cipher`を２回適用すると復号もできると気づくことが求められます．

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

ニコニコ大百科が詳しい．[Typoglycemia](https://dic.nicovideo.jp/a/typoglycemia)

```python:コード
import random as rd
```

```python:コード
def shuffle_str(x):
    x = list(x)
    rd.shuffle(x)
    return ''.join(x)

def typoglycemia(x):
    if len(x) <= 4:
        return x
    return x[0] + shuffle_str(x[1:-1]) + x[-1]
```

```python:コード
x = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
x = x.split(' ')
x = [typoglycemia(word) for word in x]
x = ' '.join(x)
x
```

```python:出力
"I c'lndout bliveee that I could allcutay uasntrdend what I was radneig : the penhnmaeol poewr of the haumn mind ."
```

長さ4以下の文字列はそのまま返し，それ以外の文字列については，先頭から2文字目から，末尾から2文字目をシャッフルします．
Pythonの文字列はイミュータブルなので，タプルと同様に代入はできません．先頭と末尾，シャッフルされた真ん中の部分をつなげて新しい文字列を作ってあげる必要があります．

## 次は第2章

[言語処理100本ノック 2020 第2章: UNIXコマンド](https://qiita.com/nymwa/items/138731ea30597036704e)