自然言語100本ノック 2025 をやってみる(00-09)

上記の「言語処理100本ノック 2025」をやってみます。
このような素晴らしい教材があるのは本当にありがたいことです。
Pythonと言語処理の基礎を身に着けることが目的です。
当方、Pythonは初心者なので気になる点あれば気兼ねなくご指摘いただけると幸いです。
2025/04/29 開始。コツコツやって追加していきます。

00.「パトカー」と「タクシー」を交互に連結
設問では文字列が同じ長さなので気にしなくても良いかもしれないですが、文字列長が違った場合でも対応できるようにしてみました。
str1 = 'パトカー'
str2 = 'タクシー'
def linked_alternately(str1 , str2):
str1_len = len(str1)
str2_len = len(str2)
maxlen = max(str1_len , str2_len)
return ''.join(
(str1[i] if str1_len > i else '') +
(str2[i] if str2_len > i else '')
for i in range(maxlen))
linked_alternately(str1 , str2)
zip_longestを使って改善
Pythonにはzip_longestという標準ライブラリがあるらしい。
以下ChatGPT先生よりzip_longestの解説。
引数に渡された複数のイテラブルなオブジェクトに対して、長さが「最長」のオブジェクトに合わせて1要素ずつ取り出せる。その際、短いイテラブルが先に尽きたらfillvalueで指定した値を代わりに出す。
便利ですね~
以下のように書き換えてみました。
from itertools import zip_longest
def linked_alternately(str1 , str2):
return ''.join(
a + b for a , b in zip_longest(str1 , str2 , fillvalue=''))
linked_alternately(str1 , str2)
感想・メモ
Javaの世界で生きてきたので、Pythonの「文字列型もイテラブルだよね」というノリがまだしっくり来ていないです。あと、文字列長の取得方法がlenメソッドなのも、フィールドアクセスじゃないんだ、って思っちゃいます。
そもそものPythonの言語設計思想がわかってないのだろうな、と思うので、こういう問題を解いて帰納的に理解していくだけでなく、Pythonの言語設計思想自体も勉強したいなあと思いました。(どうやって勉強していこう)

01.「パタトクカシーー」の2, 4, 6, 8文字目を取り出す
まずは自力で。以下のように書いてみました。
text = 'パタトクカシーー'
''.join(text[i] for i in range(len(text)) if i % 2 == 0)
改善:range(len(str))よりenumerate(str)のほうが良い
ChatGPT先生にレビューしてもらったところ、enumerate(str)のがいいよと指摘してもらいました。Pythonにはenumerateという、引数のイテラブルなオブジェクトの要素とインデックスのタプルをイテレーティブに取り出せる関数があるんですね。便利~
以下のように改善してみました。
text = 'パタトクカシーー'
''.join(c for i , c in enumerate(text) if i % 2 == 0)
別解:スライスでいいじゃん
スライスで2文字ごとに取り出せばよかったです。
text = 'パタトクカシーー'
text[::2]
感想・メモ
enumrateはいろいろな用途に応用できるのだろうと思いました。活用方法含めて体で覚えていきたいです。
スライスの記法自体は知っているのですが、アイデアとして出てこないので、まだ知識として知っているだけで身にはついていないのだろうなと思いました。

02.文字列"stressed"の文字を逆にする
まずは自力で。スライスでできるよねってことで。
text = "stressed"
text[::-1]
拡張しやすい書き方は別にあるかもしれないですが、ひとまずこれ以上ない気がします。

03.文章を単語に分解して、各単語の文字数を先頭から出現順に並べる
まずは自力で書いてみます。
text = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
omit_symbol_list = [',' , '.'] # 除外したい記号を定義
text_without_symbol = text # 記号除外後の文字列
# 文字列除外
for omit_symbol in omit_symbol_list:
text_without_symbol = text_without_symbol.replace(omit_symbol , '')
[len(x) for x in text_without_symbol.split(' ')]
一応拡張性を考慮して除外したい記号を外だししてみました。
しかし、除外したい記号分ループして、テキスト全体を繰り返し処理してしまっているのが微妙です。
改善:maketransで変換テーブルを作成して一気にtranslate
変換テーブルを作成して一気に変換すれば、記号の数だけループせずに済みました。
text = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
omit_symbols = ',.'
omit_table = str.maketrans('' , '' , omit_symbols)
text_without_symbol = text.translate(omit_table)
[len(x) for x in text_without_symbol.split(' ')]
感想・メモ
str.maketrans関数を純粋に知らなかったのですが、第一引数に置換元の文字列、第二引数に置換先の文字列、第三引数に削除対象の文字を設定することができるのですね。
置換前後を辞書型で渡すこともできるそうなので、データ整形に非常に有用そうです。

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文字を取り出し、取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ。
まずは自力で書いてみる。
text = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
one_char_word_num = [1, 5, 6, 7, 8, 9, 15, 16, 19]
word_list = text.split(" ")
element_symbol_map = {}
for i , word in enumerate(word_list):
if i + 1 in one_char_word_num:
element_symbol_map[i + 1] = word[:1]
else:
element_symbol_map[i + 1] = word[:2]
print(element_symbol_map)
改善:辞書内包表記で短く書いてみよう
辞書型も内包表記で書けるらしい。練習がてら書いてみる。
text = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
one_char_word_num = [1, 5, 6, 7, 8, 9, 15, 16, 19]
element_symbol_map = {
i + 1 : word[:1] if i + 1 in one_char_word_num else word[:2] for i , word in enumerate(word_list)
}
print(element_symbol_map)
感想・メモ
辞書型内包表記は書いてて(書けたら)気持ちよいが、可読性は微妙だな~って思いました。

05. n-gram
与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ。この関数を用い、"I am an NLPer"という文から文字tri-gram、単語bi-gramを得よ。
n-gramってなんでしょう…?
n-gramとは
ChatGPT先生に聞いてみます。
n-gramとは、テキストなどの連続するデータから取り出される「連続するn個の要素」のこと。自然言語処理(NLP)でよく使われる。
わかったような、わからないような…
ともかく自力で実装してみます。イメージ的にはこういうことであってますよね…?
text = "I am an NLPer"
def ngram(n , text , is_char_ngram=False):
if is_char_ngram:
return [ text[i:i+n] for i ,_ in enumerate(text) ]
else:
word_list = text.split(" ")
return [ word_list[i:i+n] for i ,_ in enumerate(word_list) ]
# 文字tri-gram
print(ngram(3, text , is_char_ngram=True))
# 単語bi-gram
print(ngram(2, text))
出力結果
['I a', ' am', 'am ', 'm a', ' an', 'an ', 'n N', ' NL', 'NLP', 'LPe', 'Per', 'er', 'r']
[['I', 'am'], ['am', 'an'], ['an', 'NLPer'], ['NLPer']]
修正:割り切れない部分が指定したnの通りになっていない
ChatGPT先生にレビューしていただき、以下指摘を得ましたので修正します。
['I a', ' am', 'am ', 'm a', ' an', 'an ', 'n N', ' NL', 'NLP', 'LPe', 'Per', 'er', 'r']
[['I', 'am'], ['am', 'an'], ['an', 'NLPer'], ['NLPer']]
だと、最後のほう'er' , 'r' とかが指定したnの値にあっていないので誤り。期待値は以下のような出力。
['I a', ' am', 'am ', 'm a', ' an', 'an ', 'n N', ' NL', 'NLP', 'LPe', 'Per']
[['I', 'am'], ['am', 'an'], ['an', 'NLPer']]
また、戻り値の形式が統一されていないので、単語ngramの時にタプル型にしたらどうか?と言われました。
以下のようにコード修正しました。
text = "I am an NLPer"
def ngram(n , text , is_char_ngram=False):
if is_char_ngram:
return [ text[i:i+n] for i in range(len(text) - n + 1) ]
else:
word_list = text.split(" ")
return [ tuple(word_list[i:i+n]) for i in range(len(word_list) - n + 1) ]
# 文字tri-gram
print(ngram(3, text , is_char_ngram=True))
# 単語bi-gram
print(ngram(2, text))
出力
['I a', ' am', 'am ', 'm a', ' an', 'an ', 'n N', ' NL', 'NLP', 'LPe', 'Per']
[('I', 'am'), ('am', 'an'), ('an', 'NLPer')]
感想・メモ
恥ずかしながらn-gramを知らず、どうアウトプットするのが良いのかがわからず迷走しました。。。

06. 集合
paraparaparadise”と”paragraph”に含まれる文字bi-gramの集合を、それぞれX,Yとして求め、XとYの和集合・積集合・差集合を求めよ。さらに、’se’というbi-gramがXおよびYに含まれるかどうかを調べよ。
まずは自力で書いてみます。集合だからsetにすればいいんですよね。
# n-gramを求める関数を定義
def ngram(n , text):
return set([ text[i:i+n] for i in range(len(text) - n + 1) ])
# それぞれの文字bi-gramをX,Yとして取得
X = ngram(2 , "paraparaparadise")
Y = ngram(2 , "paragraph")
# 和集合
print(X | Y)
# 積集合
print(X & Y)
# 差集合
print(X - Y)
# "se"というbi-gramが含まれるか
print("X:" , "se" in X)
print("Y:" , "se" in Y)
出力
{'pa', 'se', 'ra', 'di', 'ar', 'ag', 'gr', 'ad', 'ph', 'ap', 'is'}
{'pa', 'ap', 'ar', 'ra'}
{'se', 'ad', 'is', 'di'}
X: True
Y: False
改善:集合内包表記にしよう
ngramを作成する関数内で、list作ってからそれをsetに変換していますが、集合内包表記を使えば最初から集合で作成できるみたいです。
修正前:
def ngram(n , text):
return set([ text[i:i+n] for i in range(len(text) - n + 1) ])
修正後:
def ngram(n , text):
return { text[i:i+n] for i in range(len(text) - n + 1) }
感想・メモ
業務アプリケーション開発においては集合なんてめったなこと使ってこなかったので、「たしかsetだったよな…?動かしてみるか…?」って、動かしてみたらあってたという感じでした。
集合でも内包表記できることは知らずでした。タプルでもできるのかな?と思い以下のように記述してみましたが、これはジェネレータ式で、「タプル内包表記」とやらはないようです。
# これはジェネレータ式!
(el for el in range(1,5))
# タプルにするならこう!
tuple(el for el in range(1,5))

07. テンプレートによる文生成
引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ。さらに、x=12, y=”気温”, z=22.4として、実行結果を確認せよ。
これは簡単そう。以下のように記述してみました。
def gen_str(x , y , z):
return "{}時の{}は{}".format(x , y , z)
print(gen_str(x=12, y="気温", z=22.4))
改善:f-stringを使って可読性向上!
Python 3.6以降なら f-string を使えるみたいです。書き換えてみました。こちらのほうが可読性高くていいですね。
def gen_str(x , y , z):
return f"{x}時の{y}は{z}"
print(gen_str(x=12, y="気温", z=22.4))

08. 暗号文
与えられた文字列の各文字を、以下の仕様で変換する関数cipherを実装せよ。
・英小文字ならば (219 - 文字コード) のASCIIコードに対応する文字に置換
・その他の文字はそのまま出力
この関数を用い、英語のメッセージを暗号化・復号化せよ。
文字⇔ASCIIコードの変換に使用する関数がわからずで調べました。ord()とchr()を使うのですね。
以下のように記述してみました。
# 暗号化
def cipher(text):
return "".join([chr(219 - ord(charcter)) if charcter.islower() else charcter
for _ , charcter in enumerate(text)])
print(cipher("Hello! 私の名前はMikeです。"))
print(cipher(cipher("Hello! 私の名前はMikeです。")))
出力
Hvool! 私の名前はMrpvです。
Hello! 私の名前はMikeです。
期待通りできていると思います。

09. Typoglycemia
スペースで区切られた単語列に対して、各単語の先頭と末尾の文字は残し、それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ。ただし、長さが4以下の単語は並び替えないこととする。適当な英語の文(例えば”I couldn’t believe that I could actually understand what I was reading : the phenomenal power of the human mind .”)を与え、その実行結果を確認せよ。
まずは自力で書いてみます。ゴリ押しですがこんな感じでいかがでしょうか?
import random
def typo_word_generator(word):
# 単語の中間部分
word_middle_part = []
# 単語の中間部分のインデックス値の集合。割り当てたらこの集合から引いていく
unassigned_char_index = { i for i in range(1 , len(word) -1)}
for _ in range(len(word) -2):
while True:
index = random.randint(1 , (len(word) -2))
if index in unassigned_char_index:
word_middle_part.append(word[index])
unassigned_char_index.remove(index)
break
return word[:1] + "".join(word_middle_part) + word[-1:]
def converter(text):
after_conversion = []
for i , word in enumerate(text.split(" ")):
if len(word) > 4:
after_conversion.append(typo_word_generator(word))
else:
after_conversion.append(word)
return " ".join(after_conversion)
text = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
converter(text)
出力
"I cud'nolt bvieele that I cuold atclauly unnetasrdd what I was raeidng : the ponmeenhal poewr of the hmuan mind ."
改善:random.shuffleを使う
ChatGPT先生に聞いてみたら、random.shuffleというメソッドを教えてくれました。
引数のリストを破壊的にランダムに並び替えてくれるみたいです。
import random
word = "abcde"
char_list = list(word)
random.shuffle(char_list)
print("".join(char_list))
以下のように改善しました。
import random
def typo_word_generator(word):
middle = list(word[1:-1])
random.shuffle(middle)
return word[:1] + "".join(middle) + word[-1:]
def converter(text):
after_conversion = []
for i , word in enumerate(text.split(" ")):
if len(word) > 4:
after_conversion.append(typo_word_generator(word))
else:
after_conversion.append(word)
return " ".join(after_conversion)
text = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
converter(text)

10以降はこちら。