言語処理100本ノック2020解いてみた①

 時間が増えたのにイベントが減り達成感やマイルストンが消失してたところに、言語処理100本ノック2020が公開されていたのでチャレンジ。他にこういう解き方もあるよ!!みたいなのを知れたら良いかなと思い公開してみます。言語処理以前にナチュラルに問題の解釈を間違っている可能性もなきにしもあらずなのだが、そしたらまぁ間違っているなあと思ってください。
 ということで今回はゆるっと第1章だけやっていきをします。


00. 文字列の逆順

解答
pythonの便利機能スライスで解きます。配列だけじゃなくて文字列にも使えて便利だな。
[start:end:step]として、範囲の先頭、末尾、間隔の3つを指定してやります。今回は間隔を逆向きにするのでstepを-1にすることで後ろ向きに参照しました。ここをマイナスにするとstartとendの関係が逆になるということは普段はあまり意識していないな。

text = "stressed"
print(text[::-1])

出力

desserts

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

解答
これも同様にスライスで解きます。1文字目から2文字間隔なので、startが1でかstepが2ですな。

text="パタトクカシーー"
print(text[1::2])

出力

タクシー

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

解答
2つの文字列のi文字目を連結し続けるということで、正解用の空の文字列を用意してforで回して足していきます。

text1 = "パトカー"
text2 = "タクシー"

ans=""
for i in range(len(text1)):
    ans+=text1[i]+text2[i]
print(ans)

折角なので、別のも試してみよう。i文字目を連結した新しい配列作ってjoinで最後に連結じゃ、1行で処理できているものの可読性がいまいちですね。zipは便利だぞ。

text1 = "パトカー"
text2 = "タクシー"
print("".join([t1+t2 for t1,t2 in zip(text1,text2)]))

出力

パタトクカシーー

03. 円周率

解答
文字数のカウントをするだけなのでlenを使います。単語に分解ということで、"."とか","をreplaceで消してから、半スペースでsplitする前処理をしてやります。案外自分は内包表記使うんだなあ。

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

出力

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]

04. 元素記号

解答
単語の分解は03同様の前処理で、今回は「1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し」とあるので、enumerateを使って、取り出した配列要素の番号をチェックしつつ条件に合うように辞書に格納していきます。enumerateは0始まりなので、問題文と整合性を取るためにi+1してます。

text = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
words = text.replace(".","").replace(",","").split()

dic = {}
for i,word in enumerate(words):
	if i+1 in [1, 5, 6, 7, 8, 9, 15, 16, 19]:
		dic[word[0]] = (i+1)
	else:
		dic[word[:2]] = (i+1)

print(dic)

出力

薄々感づいているんだけどnoteのコードブロックなんかいまいちだな…。

{'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}

05. n-gram

解答
n-gramのかっこいい書き方を見つけたので参考にしました。先人の知恵は使うもの。ググりこそ力。スライスで現在の語からn語を引っ張ってくる感じですね。配列要素を飛び越えないように範囲をうまく指定してやります。
https://qiita.com/kazmaw/items/4df328cba6429ec210fb
単語bi-gramと文字bi-gramはそのまま単語単位か文字単位かで2単語or文字ずつを取ればOKかな?

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

text = "I am an NLPer"

print(n_gram(text.split(),2))
print(n_gram(text,2))

出力

[['I', 'am'], ['am', 'an'], ['an', 'NLPer']]
['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']

06. 集合

解答
n-gramには引き続きお世話になります。足を向けて寝られない。pythonはそのまま論理記号が使えてしまうのでそれで和、積、差をサクッと求めて行きます。True/Falseも条件式をそのまま書けば結果が帰ってくるので今回はそれで。

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

word1 = "paraparaparadise"
word2 = "paragraph"

X = set(n_gram(word1,2))
Y = set(n_gram(word2,2))

print("和集合",X|Y)
print("積集合",X&Y)
print("差集合",X-Y) 
print("'se' in X or Y ? ->", 'se' in X|Y)

出力

和集合 {'di', 'gr', 'ar', 'ra', 'is', 'se', 'ad', 'ph', 'ap', 'ag', 'pa'}
積集合 {'ap', 'ar', 'ra', 'pa'}
差集合 {'di', 'se', 'ad', 'is'}
'se' in X or Y ? -> True

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

解答
文字の埋め込み方法は色々派閥があると思いますが、文字列に%sを入れていくスタイルで。x,y,zの型を特に意識しなくていいので楽だなpythonは。

def template(x,y,z):
	return "%s時の%sは%s"%(x,y,z)

x = 12
y = "気温"
z = 22.4

print(template(x,y,z))

他にもいろんな書き方してみるとこんな感じですかね。f文字列、format型、辞書型…あと+でつなぐとかもできるのかな。f文字列やformatで書いた方がpythonっぽい見た目な気がするんですが、どうですか。


def template(x,y,z):
    return f"{x}時の{y}{z}"

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

def template(x,y,z):
    return "%(a)s時の%(b)sは%(c)s"%dict(a=x,b=y,c=z)

出力

12時の気温は22.4

08. 暗号文

解答
ord:文字→アスキーコード
chr:アスキーコード→文字
ということを利用して暗号化と復号化を試してみます。
小文字判定はislower()を使います。

def cipher(text):
	return "".join([chr(219-ord(t)) if t.islower() else t for t in text])

text = "This is a ciphertext."
en = cipher(text)
print(en)
de = cipher(en)
print(de)

出力

Tsrh rh z xrksvigvcg.
This is a ciphertext.

上が暗号文で下が復号文なのでちゃんと戻せてそうですね。

09. Typoglycemia

解答
まず長さが4以下かどうかを判定し、4以下ならそのまま、4以上なら先頭と末尾を抜いた文字をランダムで並び変えるという関数を用意します。文字列をrandom.sampleに突っ込むと、list型で帰ってきてしまうのでjoinで連結…という回りくどいことをしていますが、これほかにやりようあるのだろうか。

import random
def random_text(word):
	if len(word)<=4:
		return word
	else:
		return word[0] + "".join(random.sample(word[1:-1], len(word[1:-1]))) + word[-1]

text = "I couldn’t believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
words = text.split()
print(" ".join([random_text(word) for word in words]))

出力

I c’dunolt bievele that I cluod atulclay usdnranetd what I was rineadg : the pmaoenhenl peowr of the human mind .
I colud’nt blveeie that I could alcatuly ursdatennd what I was raidneg : the poeamenhnl peowr of the hmaun mind .

ランダムなので毎回並びは違うんですが、なんとなく読めてしまうな…Typoglycemiaの研究には明るくないんですが、速読とかする人ってだいたいこんな感じで文字を認識してたりするのかなあ。不思議だなあ。

感想

 3行くらいで気づいたことなんですけど、noteはコード書くのには向いてない。
これより後ろのは複雑になりそうなんでもうかける気がしない…。他人にコードを読まれる機会が実はそこまで多くない生活をしているのですが、流石にこの分量ならポイズンクッキングみたいなコードにならずに済んでいるんだろうか…どうですか、ね。

ところで、NLPってNatural Language ProcessingだけじゃなくてNeuro-Linguistic Programmingの略でもあるんですね、noteだとこっちのほうが多そうな。やっぱりQiitaに書くべきだったか…。

創作活動及び成果の発信などの形でみなさんにお返しできたらと思います。