advent calendar初日だが特別感なく書きやすいものを書く
これは
言語処理100本ノックを解いたので解答と感想を1問ずつ書いていくもの(現在11/30 PM20:30なので書けるところまでが前編)
前提条件
- 環境
- Dockerfileのリンク(関係ないのも入ってる)
- 実力
- mecab, gensimくらいはなにもわからず触ったことがある
- 解き方
- とりあえず自力で解き、10問ごとに不安だったところをググって確認した
- ので、2通りの答えを載せる部分もある
- とりあえず自力で解き、10問ごとに不安だったところをググって確認した
- 言い訳
- まとめている途中で気づいた誤答の訂正が間に合っていない
- 感謝
- こういった教材を公開していただけるのは独学で闇の中を進む者としてはとてもありがたいです
反省
この記事を描くために自分でコードを読み直していたので、擬似コードレビューとなり
-
"
と'
が混在 - rowのrと、lineのlが混在
などなど多くの改善点が判明した
本編
第1章: 準備運動
00 文字列の逆順
文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.
## 00
smt = "stressed"
ans = ""
for i in range(len(smt)):
ans += smt[-i - 1]
print(ans)
知らなかった書き方が↓
## 00
smt = "stressed"
smt[::-1]
つまりlist[start:stop:step]
ということでした
01 「パタトクカシーー」
「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.
## 1
smt = "パタトクカシーー"
''.join([smt[i] for i in range(len(smt)) if i % 2==0])
この時点では知らなかったのでこれも書き直す↓
## 1
smt = "パタトクカシーー"
smt[::2]
02 「パトカー」+「タクシー」=「パタトクカシーー」
「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.
## 2
smt1 = "パトカー"
smt2 = "タクシー"
''.join([p + t for p, t in zip(smt1, smt2)])
なんかゴリゴリ感がある
03 円周率
"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.
## 3
smt = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
[len(w) - w.count(',') - w.count('.') for w in smt.split(' ')]
isalpha()
を使ってうまく書きたかったが内包表記での二重ループをうまくできなくて一時的にこれを解答とした
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文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.
## 4
smt = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
dic = {}
target_index = [1, 5, 6, 7, 8, 9, 15, 16, 19]
for i, w in enumerate(smt.split(' ')):
if i + 1 in target_index:
dic[i + 1] = w[0]
else:
dic[i + 1] = w[:2]
dic
targetとか決め打ちでいいのか?とかifで分けていいのか?とか疑心暗鬼がやばすぎるがこのまま進む
05 n-gram
与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.
## 5
def get_n_gram(n, smt):
words = smt.split(' ')
return [smt[i:i+n] for i in range(len(smt) - n + 1)], [' '.join(words[i:i+n]) for i in range(len(words) -n + 1)]
get_n_gram(3, "I am an NLPer")
スライスで上手く書けたな〜と思ったのと文字と単語分けたほうがいいかも
06 集合
"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.
## 6
smt1 = "paraparaparadise"
smt2 = "paragraph"
X = set()
for i in range(len(smt1) - 2 + 1):
X.add(smt1[i:i+2])
Y = set()
for i in range(len(smt2) - 2 + 1):
Y.add(smt2[i:i+2])
print(X | Y)
print(X & Y)
print(X - Y)
print('se' in (X and Y))
print('se' in (X or Y))
内包表記でかけそう、、
Setが重複を消せるのを再確認したのと、listからuniqueなものが欲しい時って一旦Setにするとかアリかも
07 テンプレートによる文生成
引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.
## 7
def get_template(x, y, z):
return "{}時の{}は{}".format(x, y, z)
get_template(12, '気温', 22.4)
これは普段から使うのでできたが、{0}などで位置指定する方法をよく忘れる
08 暗号文
与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.
英小文字ならば(219 - 文字コード)の文字に置換
その他の文字はそのまま出力
この関数を用い,英語のメッセージを暗号化・復号化せよ.
## 8
class Coder:
def __init__():
pass
def encode(smt):
code = ""
for i in range(len(smt)):
if smt[i] .isalpha() and smt[i].islower():
code += chr(219 - ord(smt[i]))
else:
code += smt[i]
return code
def decode(code):
stm = ""
for i in range(len(code)):
if code[i] .isalpha() and code[i].islower():
smt += chr(219 - ord(code[i]))
else:
smt += code[i]
return smt
coder = Coder
smt = "I couldn't believe that"
code = coder.encode(smt)
desmt = coder.encode(code)
print(smt)
print(code)
print(desmt)
目が悪くてcipherをこの瞬間までcoderだと見間違えていた、しかもclassではなく関数でした
そして、文字コードは何度調べても忘れるので今度まとめたい
09 Typoglycemia
スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え,その実行結果を確認せよ.
## 9
import random
def feel_typoglycemia(smt):
typogly = []
for w in smt.split(' '):
if len(w) <= 4:
typogly.append(w)
else:
mid = list(w)[1:-1]
random.shuffle(mid)
typogly.append(w[0] + ''.join(mid) + w[-1])
return ' '.join(typogly)
smt = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
feel_typoglycemia(smt)
スライスで先頭末尾だけ外して、混ぜて、装着する処理にした
当時の記憶がないので関数名の由来がわからないが、公開する予定がなくその時の感情でつけたと思われる
第2章: UNIXコマンドの基礎
タイトル的にコマンドの確認かと思い込んでいたが、UNIXコマンドとはあくまでプログラムの実行結果の確認用だった
hightemp.txtは,日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,hightemp.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.
10 行数のカウント
行数をカウントせよ.確認にはwcコマンドを用いよ.
## 10
with open('./hightemp.txt',) as f:
print(len([r for r in f.read().split('\n') if r is not '']))
## 10
cat hightemp.txt | wc -l
rはrowからだと思うがこのあとlineのlと混合していく
11 タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.
## 11
with open('./hightemp.txt',) as f:
print([r.replace('\t', ' ') for r in f.read().split('\n') if r is not ''])
## 11
cat hightemp.txt | sed "s/\t/\ /g"
## 11
cat hightemp.txt | tr "\t" "\ "
## 11
expand -t 1 hightemp.txt
sedはvimでよくつかうやつなのでわかった、tr, expandは学びだった
12 1列目をcol1.txtに,2列目をcol2.txtに保存
各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.
## 12
with open('./hightemp.txt',) as f:
table = [r for r in f.read().split('\n') if r is not '']
with open('col1.txt', mode='w') as f:
for t in table:
f.write(t.split('\t')[0] + '\n')
with open('col2.txt', mode='w') as f:
for t in table:
f.write(t.split('\t')[1] + '\n')
## 12
cat hightemp.txt | sed "s/\t/\ /g" | cut -f 1 -d " " > col1.txt
cat hightemp.txt | sed "s/\t/\ /g" | cut -f 2 -d " " > col2.txt
縦の操作のイメージが分かずに愚直にやった
13 col1.txtとcol2.txtをマージ
12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ.
## 13
with open('cols.txt', mode='w') as c:
with open('col1.txt') as f:
with open('col2.txt') as ff:
r1 = f.readline()
r2 = ff.readline()
c.write(r1.replace('\n', '') + '\t' + r2)
while r1:
while r2:
r1 = f.readline()
r2 = ff.readline()
c.write(r1.replace('\n', '') + '\t' + r2)
## 13
paste col1.txt col2.txt > cols.txt
cat cols.txt
f, ffから滲み出るダメ感
pasteは学びだった
14 先頭からN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.
## 14
n = 5
with open('./hightemp.txt') as f:
lines = f.read()
for l in lines.split('\n')[:n]:
print(l)
head -n 5 hightemp.txt
コマンドライン引数の部分を失念していたので明確な誤答です、sys.argv
を用いたものを追記する予定です
15 末尾のN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.
## 15
n = 5
with open('./hightemp.txt') as f:
lines = f.read()
for l in lines.split('\n')[-n:]:
print(l)
## 15
tail -n 5 hightemp.txt
同じく、コマンドライン引数の部分を失念していたので明確な誤答です、sys.argv
を用いたものを追記する予定です
16 ファイルをN分割する
自然数Nをコマンドライン引数などの手段で受け取り,入力のファイルを行単位でN分割せよ.同様の処理をsplitコマンドで実現せよ.
## 16
import math
with open('./hightemp.txt') as f:
obj = f.read()
lines = [ l for l in obj.split('\n')]
n = 3
ni = math.ceil(len(lines) / n)
for i in range(0, len(lines), ni):
j = i + ni
print(len(lines[i:j]))
## 16
split -n 5 hightemp.txt
同じく、コマンドライン引数の部分を失念していたので明確な誤答です、sys.argv
を用いたものを追記する予定です
17 1列目の文字列の異なり
1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはsort, uniqコマンドを用いよ.
## 17
with open('./hightemp.txt') as f:
obj = f.read()
set(row.split('\t')[0] for row in obj.split('\n') if not row =='')
## 17
cat hightemp.txt | sed "s/\t/\ /g" | cut -f 1 -d " " | sort | uniq
こんなにパイプでつなげたのは初めてなのでワンライナの悦びを識った
18 各行を3コラム目の数値の降順にソート
各行を3コラム目の数値の逆順で整列せよ(注意: 各行の内容は変更せずに並び替えよ).確認にはsortコマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい).
## 18
with open('./hightemp.txt') as f:
obj = f.read()
rows = [row for row in obj.split('\n') if not row =='']
sorted(rows, key=lambda x: -1 * float(x.split('\t')[2]))
## 18
cat hightemp.txt | sed "s/\t/\ /g" | sort -r -k 3 -t " "
floatにキャストが必要だった
19 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる
各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.
## 19
with open('./hightemp.txt') as f:
obj = f.read()
rows =[row.split('\t')[0] for row in obj.split('\n') if not row =='']
c_dic= {}
for k in set(rows):
c_dic[k] = rows.count(k)
sorted(c_dic.items(), key=lambda x: -x[1])
## 19
cat hightemp.txt | sed "s/\t/\ /g" | cut -f 1 -d " " | sort | uniq -c | sort -rn -k 3 -t " "
rだったりrowだったりするのは反省点
第3章: 正規表現
Wikipediaの記事を以下のフォーマットで書き出したファイルjawiki-country.json.gzがある.
1行に1記事の情報がJSON形式で格納される
各行には記事名が"title"キーに,記事本文が"text"キーの辞書オブジェクトに格納され,そのオブジェクトがJSON形式で書き出される
ファイル全体はgzipで圧縮される
以下の処理を行うプログラムを作成せよ.
あまり正規表現を使わないですり抜けてしまった感がある
wget http://www.cl.ecei.tohoku.ac.jp/nlp100/data/jawiki-country.json.gz
juypter notebook上で実行していたので先頭に!をつけて実行
20 JSONデータの読み込み
Wikipedia記事のJSONファイルを読み込み,「イギリス」に関する記事本文を表示せよ.問題21-29では,ここで抽出した記事本文に対して実行せよ.
## 20
import json, gzip
with gzip.open('jawiki-country.json.gz', 'rt') as f:
obj = json.loads(f.readline())
while(obj):
try:
obj = json.loads(f.readline())
if obj['title'] == "イギリス":
break
except:
obj = f.readline()
gzipは完全に知らなかったので学び
21 カテゴリ名を含む行を抽出
記事中でカテゴリ名を宣言している行を抽出せよ.
## 21
for l in obj['text'].split('\n'):
if 'Category' in l:
print(l)
もっと厳密な条件の方がいいのかもしれない
22 カテゴリ名の抽出
記事のカテゴリ名を(行単位ではなく名前で)抽出せよ.
## 22
import re
head_pattern = r'\[\[Category:'
tail_pattern = r'\|?\*?\]\]'
for l in obj['text'].split('\n'):
if 'Category' in l:
l = re.sub(head_pattern, '', l)
print(re.sub(tail_pattern, '', l))
ゴリゴリに書いてしまった
23 セクション構造
記事中に含まれるセクション名とそのレベル(例えば"== セクション名 =="なら1)を表示せよ.
## 23
pattern = '=='
for l in obj['text'].split('\n'):
if pattern in l:
pat_by_sec = ''.join([r'=' for i in range(int(l.count('=') / 2 ))])
sec = len(pat_by_sec) - 1
tab = ''.join(['\t' for i in range(sec - 1)])
print('{}{}. {}'.format(tab, sec, l.replace('=', '')))
表示するときにタブでインデントしたかったがために少し遠回りしている
24 ファイル参照の抽出
記事から参照されているメディアファイルをすべて抜き出せ.
## 24
for l in obj['text'].split('\n'):
if 'ファイル' in l:
print(l.split(':')[1].split('|')[0])
ここもより厳密なif文がいいかも
25 テンプレートの抽出
記事中に含まれる「基礎情報」テンプレートのフィールド名と値を抽出し,辞書オブジェクトとして格納せよ.
## 25
import re
pattern = r' = '
basic_info = {}
for l in obj['text'].split('\n'):
if pattern in l:
basic_info[l.split(' = ')[0].replace('|', '')] = l.split(' = ')[1]
basic_info
テキスト処理のときによくメソッドをつなげまくってしまうのはよくなさそう
26 強調マークアップの除去
25の処理時に,テンプレートの値からMediaWikiの強調マークアップ(弱い強調,強調,強い強調のすべて)を除去してテキストに変換せよ(参考: マークアップ早見表).
## 26
import re
pattern = r' = '
basic_info = {}
for l in obj['text'].split('\n'):
if pattern in l:
basic_info[l.split(' = ')[0].replace('|', '')] = l.split(' = ')[1].replace('\'', '')
basic_info
テキスト処理には汎用性を求めずに、ゴリゴリハードコーディングを進めていいと思い始めた
27 内部リンクの除去
26の処理に加えて,テンプレートの値からMediaWikiの内部リンクマークアップを除去し,テキストに変換せよ(参考: マークアップ早見表).
## 27
import re
pattern = r' = '
med_link = r'\[|\]'
basic_info = {}
for l in obj['text'].split('\n'):
if pattern in l:
val = l.split(' = ')[1].replace('\'', '')
val = re.sub(med_link, '', val)
basic_info[l.split(' = ')[0].replace('|', '')] = val
basic_info
出力見ながら場当たり的に修正を重ねていた
28 MediaWikiマークアップの除去
27の処理に加えて,テンプレートの値からMediaWikiマークアップを可能な限り除去し,国の基本情報を整形せよ.
## 28
import re
pattern = r' = '
med_link = r'\[|\]'
strong = r'\{|\}'
tag = r'\<+.*\>'
basic_info = {}
for l in obj['text'].split('\n'):
if pattern in l:
val = l.split(' = ')[1].replace('\'', '')
val = re.sub(med_link, '', val)
val = re.sub(strong, '', val)
val = re.sub(tag, '', val)
basic_info[l.split(' = ')[0].replace('|', '')] = val
basic_info
「可能な限り」なのでギブアップが早かった
29 国旗画像のURLを取得する
テンプレートの内容を利用し,国旗画像のURLを取得せよ.(ヒント: MediaWiki APIのimageinfoを呼び出して,ファイル参照をURLに変換すればよい)
## 29
import requests
S = requests.Session()
URL = "https://en.wikipedia.org/w/api.php"
PARAMS = {
"action": "query",
"format": "json",
"prop": "imageinfo",
"iiprop": "url",
"titles": "File:" + basic_info['国旗画像']
}
R = S.get(url=URL, params=PARAMS)
DATA = R.json()
PAGES = DATA["query"]["pages"]
for k, v in PAGES.items():
for kk, vv in v.items():
if kk == 'imageinfo':
print(vv[0]['url'])
リファレンスのコードを参考にしてapiを叩いた
第4章: 形態素解析
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をMeCabを使って形態素解析し,その結果をneko.txt.mecabというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.
なお,問題37, 38, 39はmatplotlibもしくはGnuplotを用いるとよい.
wget http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt
import MeCab
t = MeCab.Tagger()
with open('./neko.txt') as f:
text = f.read()
with open('./neko.txt.mecab', mode='w') as f:
f.write(t.parse(text))
今までは解析結果を保存せずに、一連の処理のなかで解析をかけていた
この運用の方が良さそうなので学び
30 形態素解析結果の読み込み
形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素(マッピング型)のリストとして表現せよ.第4章の残りの問題では,ここで作ったプログラムを活用せよ.
## 30
doc = []
with open('./neko.txt.mecab') as f:
token_list = []
token = f.readline()
while('EOS' not in token):
dic = {}
dic['surface'] = token.split('\t')[0]
dic['base'] = token.split('\t')[1].split(',')[-3]
dic['pos'] = token.split('\t')[1].split(',')[0]
dic['pos1'] = token.split('\t')[1].split(',')[1]
token = f.readline()
if dic['surface'] == '。':
doc.append(token_list)
token_list = []
continue
token_list.
token.split('\t')
の返り値を一回格納した方がいいかも
31 動詞
動詞の表層形をすべて抽出せよ.
## 31
for s in doc:
for t in s:
if t['pos'] == '動詞':
print(t['surface'])
今だったら絶対に[t['surface'] for t in s if t['pos'] == '動詞']
と書きます
32 動詞の原形
動詞の原形をすべて抽出せよ.
## 32
for s in doc:
for t in s:
if t['pos'] == '動詞':
print(t['base'])
同じく[t['base'] for t in s if t['pos'] == '動詞']
33 サ変名詞
サ変接続の名詞をすべて抽出せよ.
## 33
for s in doc:
for t in s:
if t['pos1'] == 'サ変接続':
print(t['base'])
同じく[t['base'] for t in s if t['pos1'] == 'サ変名詞']
34 「AのB」
2つの名詞が「の」で連結されている名詞句を抽出せよ.
## 34
for s in doc:
for i, t in enumerate(s):
if t['surface'] == 'の' and i + 1 != len(s):
if s[i -1]['pos'] == '名詞' and s[i +1]['pos'] == '名詞':
print(s[i -1]['surface'] + t['base'] + s[i +1]['surface'])
形態素「の」から始まる文がないとして、indexが後ろに飛び出さないように気をつけている
形態素「の」から始まる文がないとして
たぶんよくない
35 名詞の連接
名詞の連接(連続して出現する名詞)を最長一致で抽出せよ.
## 35
## 35
max_list = []
tmp = ""
max_len = len(tmp)
for s in doc:
for i, t in enumerate(s):
if t['pos'] == '名詞' :
tmp += t['surface']
else:
if len(tmp) == max_len:
max_list.append(tmp)
elif len(tmp) > max_len:
max_list = []
max_list.append(tmp)
max_len = len(tmp)
tmp = ''
print(len(max_list[0]))
print(max_list)
英単語で30文字でした
36 単語の出現頻度
文章中に出現する単語とその出現頻度を求め,出現頻度の高い順に並べよ.
## 36
base_list = []
count_dic = {}
for s in doc:
for t in s:
base_list.append(t['base'])
for word in set(base_list):
count_dic[word] = base_list.count(word)
sorted(count_dic.items(), key=lambda x: -x[1])
base_list = [t['base'] for s in doc for t in s]
でいけそう
37 頻度上位10語
出現頻度が高い10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.
## 37
import matplotlib.pyplot as plt
import japanize_matplotlib
%matplotlib inline
n = 10
labels = [i[0] for i in sorted(count_dic.items(), key=lambda x: -x[1])[:n]]
score = [i[1] for i in sorted(count_dic.items(), key=lambda x: -x[1])[:n]]
plt.bar(labels, score)
plt.show()
matplotlibでの日本語表示のためにフォントを設定したりしてハマって心折れていたらjapanize-matplotlibという良いものに出会えました
38 ヒストグラム
単語の出現頻度のヒストグラム(横軸に出現頻度,縦軸に出現頻度をとる単語の種類数を棒グラフで表したもの)を描け.
## 38
import matplotlib.pyplot as plt
import japanize_matplotlib
%matplotlib inline
all_score = [i[1] for i in sorted(count_dic.items(), key=lambda x: -x[1])]
plt.hist(all_score, range(10, 100));
この辺りから辞書のリストのソートに慣れた顔をしだす
39 Zipfの法則
単語の出現頻度順位を横軸,その出現頻度を縦軸として,両対数グラフをプロットせよ.
## 39
import math
log_idx = [math.log(i + 1) for i in range(len(count_dic.values()))]
log_all_score = [math.log(i[1]) for i in sorted(count_dic.items(), key=lambda x: -x[1])]
plt.scatter(log_idx, log_all_score, range(10, 100));
知らなかったので出力見てすげ〜となった
numpyではなくmathをつかった
おわり
こういうのって問題を掲載して良いのだろうか、だめであればすぐ消します
ゼミなどのコミュニティがあれば、毎週10問とか決めて全体でレビューし合えば良さそう
アドベントカレンダー中にラストまでまとめたいですね〜